Merge branch 'master' into feature_setting_visibility_profiles

This commit is contained in:
fieldOfView 2018-02-08 16:06:48 +01:00
commit 69d7bf4130
414 changed files with 40051 additions and 22967 deletions

1
.gitignore vendored
View file

@ -47,7 +47,6 @@ plugins/Doodle3D-cura-plugin
plugins/FlatProfileExporter
plugins/GodMode
plugins/OctoPrintPlugin
plugins/PostProcessingPlugin
plugins/ProfileFlattener
plugins/X3GWriter

2
Jenkinsfile vendored
View file

@ -1,5 +1,5 @@
timeout(time: 2, unit: "HOURS") {
parallel_nodes(['linux && cura', 'windows && cura']) {
timeout(time: 2, unit: "HOURS") {
// Prepare building
stage('Prepare') {
// Ensure we start with a clean build directory.

View file

@ -41,25 +41,7 @@ Please check out [Wiki page](https://github.com/Ultimaker/Cura/wiki/Cura-Setting
Translating Cura
----------------
If you'd like to contribute a translation of Cura, please first look for [any existing translation](https://github.com/Ultimaker/Cura/tree/master/resources/i18n). If your language is already there in the source code but not in Cura's interface, it may be partially translated.
There are four files that need to be translated for Cura:
1. https://github.com/Ultimaker/Cura/blob/master/resources/i18n/cura.pot
2. https://github.com/Ultimaker/Cura/blob/master/resources/i18n/fdmextruder.def.json.pot
3. https://github.com/Ultimaker/Cura/blob/master/resources/i18n/fdmprinter.def.json.pot (This one is the most work.)
4. https://github.com/Ultimaker/Uranium/blob/master/resources/i18n/uranium.pot
Copy these files and rename them to `*.po` (remove the `t`). Then create the actual translations by filling in the empty `msgstr` entries. These are gettext files, which are plain text so you can open them with any text editor such as Notepad or GEdit, but it is probably easier with a specialised tool such as [POEdit](https://poedit.net/) or [Virtaal](http://virtaal.translatehouse.org/).
Do not hestiate to ask us about a translation or the meaning of some text via Github Issues.
Once the translation is complete, it's probably best to test them in Cura. Use your favourite software to convert the .po file to a .mo file (such as [GetText](https://www.gnu.org/software/gettext/)). Then put the .mo files in the `.../resources/i18n/<language code>/LC_MESSAGES` folder in your Cura installation. Then find your Cura configuration file (next to the log as described above, except on Linux where it is located in `~/.config/cura`) and change the language preference to the name of the folder you just created. Then start Cura. If working correctly, your Cura should now be translated.
To submit your translation, ideally you would make two pull requests where all `*.po` files are located in that same `<language code>` folder in the resources of both the Cura and Uranium repositories. Put `cura.po`, `fdmprinter.def.json.po` and `fdmextruder.def.json.po` in the Cura repository, and put `uranium.po` in the Uranium repository. Then submit the pull requests to Github. For people with less experience with Git, you can also e-mail the translations to the e-mail address listed at the top of the [cura.pot](https://github.com/Ultimaker/Cura/blob/master/resources/i18n/cura.pot) file as the `Report-Msgid-Bugs-To` entry and we'll make sure it gets checked and included.
After the translation is submitted, the Cura maintainers will check for its completeness and check whether it is consistent. We will take special care to look for common mistakes, such as translating mark-up `<message>` code and such. We are often not fluent in every language, so we expect the translator and the international users to make corrections where necessary. Of course, there will always be some mistakes in every translation.
When the next Cura release comes around, some of the texts will have changed and some new texts will have been added. Around the time when the beta is released we will invoke a string freeze, meaning that no developer is allowed to make changes to the texts. Then we will update the translation template `.pot` files and ask all our translators to update their translations. If you are unable to update the translation in time for the actual release, we will remove the language from the drop-down menu in the Preferences window. The translation stays in Cura however, so that someone might pick it up again later and update it with the newest texts. Also, users who had previously selected the language can still continue Cura in their language but English text will appear among the original text.
Please check out [Wiki page](https://github.com/Ultimaker/Cura/wiki/Translating-Cura) about how to translate Cura into other languages.
License
----------------

View file

@ -88,3 +88,5 @@ class ArrangeObjectsJob(Job):
no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"),
title = i18n_catalog.i18nc("@info:title", "Can't Find Location"))
no_full_solution_message.show()
self.finished.emit(self)

View file

@ -29,8 +29,12 @@ class ShapeArray:
offset_x = int(numpy.amin(flip_vertices[:, 1]))
flip_vertices[:, 0] = numpy.add(flip_vertices[:, 0], -offset_y)
flip_vertices[:, 1] = numpy.add(flip_vertices[:, 1], -offset_x)
shape = [int(numpy.amax(flip_vertices[:, 0])), int(numpy.amax(flip_vertices[:, 1]))]
shape = numpy.array([int(numpy.amax(flip_vertices[:, 0])), int(numpy.amax(flip_vertices[:, 1]))])
shape[numpy.where(shape == 0)] = 1
arr = cls.arrayFromPolygon(shape, flip_vertices)
if not numpy.ndarray.any(arr):
# set at least 1 pixel
arr[0][0] = 1
return cls(arr, offset_x, offset_y)
## Instantiate an offset and hull ShapeArray from a scene node.

View file

@ -1,6 +1,7 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Settings.ExtruderManager import ExtruderManager
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.i18n import i18nCatalog
@ -25,7 +26,7 @@ catalog = i18nCatalog("cura")
import numpy
import math
from typing import List
from typing import List, Optional
# Setting for clearance around the prime
PRIME_CLEARANCE = 6.5
@ -73,6 +74,11 @@ class BuildVolume(SceneNode):
self._adhesion_type = None
self._platform = Platform(self)
self._build_volume_message = Message(catalog.i18nc("@info:status",
"The build volume height has been reduced due to the value of the"
" \"Print Sequence\" setting to prevent the gantry from colliding"
" with printed models."), title = catalog.i18nc("@info:title", "Build Volume"))
self._global_container_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onStackChanged)
self._onStackChanged()
@ -96,11 +102,6 @@ class BuildVolume(SceneNode):
self._setting_change_timer.setSingleShot(True)
self._setting_change_timer.timeout.connect(self._onSettingChangeTimerFinished)
self._build_volume_message = Message(catalog.i18nc("@info:status",
"The build volume height has been reduced due to the value of the"
" \"Print Sequence\" setting to prevent the gantry from colliding"
" with printed models."), title = catalog.i18nc("@info:title","Build Volume"))
# Must be after setting _build_volume_message, apparently that is used in getMachineManager.
# activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality.
# Therefore this works.
@ -241,6 +242,44 @@ class BuildVolume(SceneNode):
for child_node in group_node.getAllChildren():
child_node._outside_buildarea = group_node._outside_buildarea
## Update the outsideBuildArea of a single node, given bounds or current build volume
def checkBoundsAndUpdate(self, node: CuraSceneNode, bounds: Optional[AxisAlignedBox] = None):
if not isinstance(node, CuraSceneNode):
return
if bounds is None:
build_volume_bounding_box = self.getBoundingBox()
if build_volume_bounding_box:
# It's over 9000!
build_volume_bounding_box = build_volume_bounding_box.set(bottom=-9001)
else:
# No bounding box. This is triggered when running Cura from command line with a model for the first time
# In that situation there is a model, but no machine (and therefore no build volume.
return
else:
build_volume_bounding_box = bounds
if node.callDecoration("isSliceable") or node.callDecoration("isGroup"):
bbox = node.getBoundingBox()
# Mark the node as outside the build volume if the bounding box test fails.
if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
node.setOutsideBuildArea(True)
return
convex_hull = self.callDecoration("getConvexHull")
if convex_hull:
if not convex_hull.isValid():
return
# Check for collisions between disallowed areas and the object
for area in self.getDisallowedAreas():
overlap = convex_hull.intersectsPolygon(area)
if overlap is None:
continue
node.setOutsideBuildArea(True)
return
node.setOutsideBuildArea(False)
## Recalculates the build volume & disallowed areas.
def rebuild(self):
if not self._width or not self._height or not self._depth:

View file

@ -12,7 +12,7 @@ class CameraImageProvider(QQuickImageProvider):
def requestImage(self, id, size):
for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
try:
return output_device.getCameraImage(), QSize(15, 15)
return output_device.activePrinter.camera.getImage(), QSize(15, 15)
except AttributeError:
pass
return QImage(), QSize(15, 15)

View file

@ -1,7 +1,6 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import sys
import platform
import traceback
import faulthandler
@ -13,9 +12,11 @@ import json
import ssl
import urllib.request
import urllib.error
import shutil
import sys
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QCoreApplication
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton
from UM.Application import Application
from UM.Logger import Logger
@ -49,10 +50,11 @@ fatal_exception_types = [
class CrashHandler:
crash_url = "https://stats.ultimaker.com/api/cura"
def __init__(self, exception_type, value, tb):
def __init__(self, exception_type, value, tb, has_started = True):
self.exception_type = exception_type
self.value = value
self.traceback = tb
self.has_started = has_started
self.dialog = None # Don't create a QDialog before there is a QApplication
# While we create the GUI, the information will be stored for sending afterwards
@ -64,21 +66,130 @@ class CrashHandler:
for part in line.rstrip("\n").split("\n"):
Logger.log("c", part)
if not CuraDebugMode and exception_type not in fatal_exception_types:
# If Cura has fully started, we only show fatal errors.
# If Cura has not fully started yet, we always show the early crash dialog. Otherwise, Cura will just crash
# without any information.
if has_started and exception_type not in fatal_exception_types:
return
application = QCoreApplication.instance()
if not application:
sys.exit(1)
if not has_started:
self._send_report_checkbox = None
self.early_crash_dialog = self._createEarlyCrashDialog()
self.dialog = QDialog()
self._createDialog()
def _createEarlyCrashDialog(self):
dialog = QDialog()
dialog.setMinimumWidth(500)
dialog.setMinimumHeight(170)
dialog.setWindowTitle(catalog.i18nc("@title:window", "Cura Crashed"))
dialog.finished.connect(self._closeEarlyCrashDialog)
layout = QVBoxLayout(dialog)
label = QLabel()
label.setText(catalog.i18nc("@label crash message", """<p><b>A fatal error has occurred.</p></b>
<p>Unfortunately, Cura encountered an unrecoverable error during start up. It was possibly caused by some incorrect configuration files. We suggest to backup and reset your configuration.</p>
<p>Please send us this Crash Report to fix the problem.</p>
"""))
label.setWordWrap(True)
layout.addWidget(label)
# "send report" check box and show details
self._send_report_checkbox = QCheckBox(catalog.i18nc("@action:button", "Send crash report to Ultimaker"), dialog)
self._send_report_checkbox.setChecked(True)
show_details_button = QPushButton(catalog.i18nc("@action:button", "Show detailed crash report"), dialog)
show_details_button.setMaximumWidth(200)
show_details_button.clicked.connect(self._showDetailedReport)
layout.addWidget(self._send_report_checkbox)
layout.addWidget(show_details_button)
# "backup and start clean" and "close" buttons
buttons = QDialogButtonBox()
buttons.addButton(QDialogButtonBox.Close)
buttons.addButton(catalog.i18nc("@action:button", "Backup and Reset Configuration"), QDialogButtonBox.AcceptRole)
buttons.rejected.connect(self._closeEarlyCrashDialog)
buttons.accepted.connect(self._backupAndStartClean)
layout.addWidget(buttons)
return dialog
def _closeEarlyCrashDialog(self):
if self._send_report_checkbox.isChecked():
self._sendCrashReport()
os._exit(1)
def _backupAndStartClean(self):
# backup the current cura directories and create clean ones
from cura.CuraVersion import CuraVersion
from UM.Resources import Resources
# The early crash may happen before those information is set in Resources, so we need to set them here to
# make sure that Resources can find the correct place.
Resources.ApplicationIdentifier = "cura"
Resources.ApplicationVersion = CuraVersion
config_path = Resources.getConfigStoragePath()
data_path = Resources.getDataStoragePath()
cache_path = Resources.getCacheStoragePath()
folders_to_backup = []
folders_to_remove = [] # only cache folder needs to be removed
folders_to_backup.append(config_path)
if data_path != config_path:
folders_to_backup.append(data_path)
# Only remove the cache folder if it's not the same as data or config
if cache_path not in (config_path, data_path):
folders_to_remove.append(cache_path)
for folder in folders_to_remove:
shutil.rmtree(folder, ignore_errors = True)
for folder in folders_to_backup:
base_name = os.path.basename(folder)
root_dir = os.path.dirname(folder)
import datetime
date_now = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
idx = 0
file_name = base_name + "_" + date_now
zip_file_path = os.path.join(root_dir, file_name + ".zip")
while os.path.exists(zip_file_path):
idx += 1
file_name = base_name + "_" + date_now + "_" + idx
zip_file_path = os.path.join(root_dir, file_name + ".zip")
try:
# remove the .zip extension because make_archive() adds it
zip_file_path = zip_file_path[:-4]
shutil.make_archive(zip_file_path, "zip", root_dir = root_dir, base_dir = base_name)
# remove the folder only when the backup is successful
shutil.rmtree(folder, ignore_errors = True)
# create an empty folder so Resources will not try to copy the old ones
os.makedirs(folder, 0o0755, exist_ok=True)
except Exception as e:
Logger.logException("e", "Failed to backup [%s] to file [%s]", folder, zip_file_path)
if not self.has_started:
print("Failed to backup [%s] to file [%s]: %s", folder, zip_file_path, e)
self.early_crash_dialog.close()
def _showDetailedReport(self):
self.dialog.exec_()
## Creates a modal dialog.
def _createDialog(self):
self.dialog.setMinimumWidth(640)
self.dialog.setMinimumHeight(640)
self.dialog.setWindowTitle(catalog.i18nc("@title:window", "Crash Report"))
# if the application has not fully started, this will be a detailed report dialog which should not
# close the application when it's closed.
if self.has_started:
self.dialog.finished.connect(self._close)
layout = QVBoxLayout(self.dialog)
@ -89,6 +200,9 @@ class CrashHandler:
layout.addWidget(self._userDescriptionWidget())
layout.addWidget(self._buttonsWidget())
def _close(self):
os._exit(1)
def _messageWidget(self):
label = QLabel()
label.setText(catalog.i18nc("@label crash message", """<p><b>A fatal error has occurred. Please send us this Crash Report to fix the problem</p></b>
@ -148,8 +262,8 @@ class CrashHandler:
layout = QVBoxLayout()
text_area = QTextEdit()
trace_dict = traceback.format_exception(self.exception_type, self.value, self.traceback)
trace = "".join(trace_dict)
trace_list = traceback.format_exception(self.exception_type, self.value, self.traceback)
trace = "".join(trace_list)
text_area.setText(trace)
text_area.setReadOnly(True)
@ -157,13 +271,27 @@ class CrashHandler:
group.setLayout(layout)
# Parsing all the information to fill the dictionary
summary = trace_dict[len(trace_dict)-1].rstrip("\n")
module = trace_dict[len(trace_dict)-2].rstrip("\n").split("\n")
summary = ""
if len(trace_list) >= 1:
summary = trace_list[len(trace_list)-1].rstrip("\n")
module = [""]
if len(trace_list) >= 2:
module = trace_list[len(trace_list)-2].rstrip("\n").split("\n")
module_split = module[0].split(", ")
filepath = module_split[0].split("\"")[1]
filepath_directory_split = module_split[0].split("\"")
filepath = ""
if len(filepath_directory_split) > 1:
filepath = filepath_directory_split[1]
directory, filename = os.path.split(filepath)
line = ""
if len(module_split) > 1:
line = int(module_split[1].lstrip("line "))
function = ""
if len(module_split) > 2:
function = module_split[2].lstrip("in ")
code = ""
if len(module) > 1:
code = module[1].lstrip(" ")
# Using this workaround for a cross-platform path splitting
@ -189,7 +317,7 @@ class CrashHandler:
json_metadata_file = os.path.join(directory, "plugin.json")
try:
with open(json_metadata_file, "r") as f:
with open(json_metadata_file, "r", encoding = "utf-8") as f:
try:
metadata = json.loads(f.read())
module_version = metadata["version"]
@ -217,9 +345,9 @@ class CrashHandler:
text_area = QTextEdit()
tmp_file_fd, tmp_file_path = tempfile.mkstemp(prefix = "cura-crash", text = True)
os.close(tmp_file_fd)
with open(tmp_file_path, "w") as f:
with open(tmp_file_path, "w", encoding = "utf-8") as f:
faulthandler.dump_traceback(f, all_threads=True)
with open(tmp_file_path, "r") as f:
with open(tmp_file_path, "r", encoding = "utf-8") as f:
logdata = f.read()
text_area.setText(logdata)
@ -249,9 +377,13 @@ class CrashHandler:
def _buttonsWidget(self):
buttons = QDialogButtonBox()
buttons.addButton(QDialogButtonBox.Close)
# Like above, this will be served as a separate detailed report dialog if the application has not yet been
# fully loaded. In this case, "send report" will be a check box in the early crash dialog, so there is no
# need for this extra button.
if self.has_started:
buttons.addButton(catalog.i18nc("@action:button", "Send report"), QDialogButtonBox.AcceptRole)
buttons.rejected.connect(self.dialog.close)
buttons.accepted.connect(self._sendCrashReport)
buttons.rejected.connect(self.dialog.close)
return buttons
@ -269,15 +401,23 @@ class CrashHandler:
kwoptions["context"] = ssl._create_unverified_context()
Logger.log("i", "Sending crash report info to [%s]...", self.crash_url)
if not self.has_started:
print("Sending crash report info to [%s]...\n" % self.crash_url)
try:
f = urllib.request.urlopen(self.crash_url, **kwoptions)
Logger.log("i", "Sent crash report info.")
if not self.has_started:
print("Sent crash report info.\n")
f.close()
except urllib.error.HTTPError:
except urllib.error.HTTPError as e:
Logger.logException("e", "An HTTP error occurred while trying to send crash report")
except Exception: # We don't want any exception to cause problems
if not self.has_started:
print("An HTTP error occurred while trying to send crash report: %s" % e)
except Exception as e: # We don't want any exception to cause problems
Logger.logException("e", "An exception occurred while trying to send crash report")
if not self.has_started:
print("An exception occurred while trying to send crash report: %s" % e)
os._exit(1)

View file

@ -73,7 +73,7 @@ class CuraActions(QObject):
# \param count The number of times to multiply the selection.
@pyqtSlot(int)
def multiplySelection(self, count: int) -> None:
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, 8)
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = 8)
job.start()
## Delete all selected objects.
@ -94,6 +94,10 @@ class CuraActions(QObject):
removed_group_nodes.append(group_node)
op.addOperation(SetParentOperation(remaining_nodes_in_group[0], group_node.getParent()))
op.addOperation(RemoveSceneNodeOperation(group_node))
# Reset the print information
Application.getInstance().getController().getScene().sceneChanged.emit(node)
op.push()
## Set the extruder that should be used to print the selection.

View file

@ -1,6 +1,7 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
#Type hinting.
from typing import Dict
from PyQt5.QtNetwork import QLocalServer
from PyQt5.QtNetwork import QLocalSocket
@ -87,6 +88,7 @@ from PyQt5.QtGui import QColor, QIcon
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
from configparser import ConfigParser
import sys
import os.path
import numpy
@ -95,6 +97,7 @@ import os
import argparse
import json
numpy.seterr(all="ignore")
MYPY = False
@ -113,6 +116,8 @@ class CuraApplication(QtApplication):
# changes of the settings.
SettingVersion = 4
Created = False
class ResourceTypes:
QmlFiles = Resources.UserType + 1
Firmware = Resources.UserType + 2
@ -133,7 +138,6 @@ class CuraApplication(QtApplication):
stacksValidationFinished = pyqtSignal() # Emitted whenever a validation is finished
def __init__(self, **kwargs):
# this list of dir names will be used by UM to detect an old cura directory
for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "user", "variants"]:
Resources.addExpectedDirNameInData(dir_name)
@ -142,6 +146,7 @@ class CuraApplication(QtApplication):
if not hasattr(sys, "frozen"):
Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources"))
self._use_gui = True
self._open_file_queue = [] # Files to open when plug-ins are loaded.
# Need to do this before ContainerRegistry tries to load the machines
@ -223,6 +228,10 @@ class CuraApplication(QtApplication):
tray_icon_name = "cura-icon-32.png",
**kwargs)
# FOR TESTING ONLY
if kwargs["parsed_command_line"].get("trigger_early_crash", False):
assert not "This crash is triggered by the trigger_early_crash command line argument."
self.default_theme = "cura-light"
self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
@ -256,7 +265,7 @@ class CuraApplication(QtApplication):
self._center_after_select = False
self._camera_animation = None
self._cura_actions = None
self._started = False
self.started = False
self._message_box_callback = None
self._message_box_callback_arguments = []
@ -266,6 +275,7 @@ class CuraApplication(QtApplication):
self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity)
self.getController().toolOperationStopped.connect(self._onToolOperationStopped)
self.getController().contextMenuRequested.connect(self._onContextMenuRequested)
self.getCuraSceneController().activeBuildPlateChanged.connect(self.updatePlatformActivity)
Resources.addType(self.ResourceTypes.QmlFiles, "qml")
Resources.addType(self.ResourceTypes.Firmware, "firmware")
@ -277,6 +287,11 @@ class CuraApplication(QtApplication):
# We need them to simplify the switching between materials.
empty_container = ContainerRegistry.getInstance().getEmptyInstanceContainer()
empty_definition_changes_container = copy.deepcopy(empty_container)
empty_definition_changes_container.setMetaDataEntry("id", "empty_definition_changes")
empty_definition_changes_container.addMetaDataEntry("type", "definition_changes")
ContainerRegistry.getInstance().addContainer(empty_definition_changes_container)
empty_variant_container = copy.deepcopy(empty_container)
empty_variant_container.setMetaDataEntry("id", "empty_variant")
empty_variant_container.addMetaDataEntry("type", "variant")
@ -298,6 +313,7 @@ class CuraApplication(QtApplication):
empty_quality_changes_container = copy.deepcopy(empty_container)
empty_quality_changes_container.setMetaDataEntry("id", "empty_quality_changes")
empty_quality_changes_container.addMetaDataEntry("type", "quality_changes")
empty_quality_changes_container.addMetaDataEntry("quality_type", "not_supported")
ContainerRegistry.getInstance().addContainer(empty_quality_changes_container)
with ContainerRegistry.getInstance().lockFile():
@ -319,7 +335,7 @@ class CuraApplication(QtApplication):
preferences.addPreference("cura/asked_dialog_on_project_save", False)
preferences.addPreference("cura/choice_on_profile_override", "always_ask")
preferences.addPreference("cura/choice_on_open_project", "always_ask")
preferences.addPreference("cura/arrange_objects_on_load", True)
preferences.addPreference("cura/not_arrange_objects_on_load", False)
preferences.addPreference("cura/use_multi_build_plate", False)
preferences.addPreference("cura/currency", "")
@ -340,57 +356,19 @@ class CuraApplication(QtApplication):
preferences.setDefault("local_file/last_used_type", "text/x-gcode")
preferences.setDefault("general/visible_settings", """
machine_settings
resolution
layer_height
shell
wall_thickness
top_bottom_thickness
z_seam_x
z_seam_y
infill
infill_sparse_density
gradual_infill_steps
material
material_print_temperature
material_bed_temperature
material_diameter
material_flow
retraction_enable
speed
speed_print
speed_travel
acceleration_print
acceleration_travel
jerk_print
jerk_travel
travel
cooling
cool_fan_enabled
support
support_enable
support_extruder_nr
support_type
platform_adhesion
adhesion_type
adhesion_extruder_nr
brim_width
raft_airgap
layer_0_z_overlap
raft_surface_layers
dual
prime_tower_enable
prime_tower_size
prime_tower_position_x
prime_tower_position_y
meshfix
blackmagic
print_sequence
infill_mesh
cutting_mesh
experimental
""".replace("\n", ";").replace(" ", ""))
setting_visibily_preset_names = self.getVisibilitySettingPresetTypes()
preferences.setDefault("general/visible_settings_preset", setting_visibily_preset_names)
preset_setting_visibility_choice = Preferences.getInstance().getValue("general/preset_setting_visibility_choice")
default_preset_visibility_group_name = "Basic"
if preset_setting_visibility_choice == "" or preset_setting_visibility_choice is None:
if preset_setting_visibility_choice not in setting_visibily_preset_names:
preset_setting_visibility_choice = default_preset_visibility_group_name
visible_settings = self.getVisibilitySettingPreset(settings_preset_name = preset_setting_visibility_choice)
preferences.setDefault("general/visible_settings", visible_settings)
preferences.setDefault("general/preset_setting_visibility_choice", preset_setting_visibility_choice)
self.applicationShuttingDown.connect(self.saveSettings)
self.engineCreatedSignal.connect(self._onEngineCreated)
@ -402,6 +380,94 @@ class CuraApplication(QtApplication):
self.getCuraSceneController().setActiveBuildPlate(0) # Initialize
CuraApplication.Created = True
@pyqtSlot(str, result = str)
def getVisibilitySettingPreset(self, settings_preset_name) -> str:
result = self._loadPresetSettingVisibilityGroup(settings_preset_name)
formatted_preset_settings = self._serializePresetSettingVisibilityData(result)
return formatted_preset_settings
## Serialise the given preset setting visibitlity group dictionary into a string which is concatenated by ";"
#
def _serializePresetSettingVisibilityData(self, settings_data: dict) -> str:
result_string = ""
for key in settings_data:
result_string += key + ";"
for value in settings_data[key]:
result_string += value + ";"
return result_string
## Load the preset setting visibility group with the given name
#
def _loadPresetSettingVisibilityGroup(self, visibility_preset_name) -> Dict[str, str]:
preset_dir = Resources.getPath(Resources.PresetSettingVisibilityGroups)
result = {}
right_preset_found = False
for item in os.listdir(preset_dir):
file_path = os.path.join(preset_dir, item)
if not os.path.isfile(file_path):
continue
parser = ConfigParser(allow_no_value = True) # accept options without any value,
try:
parser.read([file_path])
if not parser.has_option("general", "name"):
continue
if parser["general"]["name"] == visibility_preset_name:
right_preset_found = True
for section in parser.sections():
if section == 'general':
continue
else:
section_settings = []
for option in parser[section].keys():
section_settings.append(option)
result[section] = section_settings
if right_preset_found:
break
except Exception as e:
Logger.log("e", "Failed to load setting visibility preset %s: %s", file_path, str(e))
return result
## Check visibility setting preset folder and returns available types
#
def getVisibilitySettingPresetTypes(self):
preset_dir = Resources.getPath(Resources.PresetSettingVisibilityGroups)
result = {}
for item in os.listdir(preset_dir):
file_path = os.path.join(preset_dir, item)
if not os.path.isfile(file_path):
continue
parser = ConfigParser(allow_no_value=True) # accept options without any value,
try:
parser.read([file_path])
if not parser.has_option("general", "name") and not parser.has_option("general", "weight"):
continue
result[parser["general"]["weight"]] = parser["general"]["name"]
except Exception as e:
Logger.log("e", "Failed to load setting preset %s: %s", file_path, str(e))
return result
def _onEngineCreated(self):
self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
@ -450,7 +516,7 @@ class CuraApplication(QtApplication):
elif choice == "always_keep":
# don't show dialog and KEEP the profile
self.discardOrKeepProfileChangesClosed("keep")
else:
elif self._use_gui:
# ALWAYS ask whether to keep or discard the profile
self.showDiscardOrKeepProfileChanges.emit()
has_user_interaction = True
@ -496,7 +562,7 @@ class CuraApplication(QtApplication):
#
# Note that the AutoSave plugin also calls this method.
def saveSettings(self):
if not self._started: # Do not do saving during application start
if not self.started: # Do not do saving during application start
return
ContainerRegistry.getInstance().saveDirtyContainers()
@ -650,12 +716,47 @@ class CuraApplication(QtApplication):
def run(self):
self.preRun()
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))
# Check if we should run as single instance or not
self._setUpSingleInstanceServer()
# Setup scene and build volume
root = self.getController().getScene().getRoot()
self._volume = BuildVolume.BuildVolume(self.getController().getScene().getRoot())
Arrange.build_volume = self._volume
# initialize info objects
self._print_information = PrintInformation.PrintInformation()
self._cura_actions = CuraActions.CuraActions(self)
# Detect in which mode to run and execute that mode
if self.getCommandLineOption("headless", False):
self.runWithoutGUI()
else:
self.runWithGUI()
# Pre-load files if requested
for file_name in self.getCommandLineOption("file", []):
self._openFile(file_name)
for file_name in self._open_file_queue: # Open all the files that were queued up while plug-ins were loading.
self._openFile(file_name)
self.started = True
self.exec_()
## Run Cura without GUI elements and interaction (server mode).
def runWithoutGUI(self):
self._use_gui = False
self.closeSplash()
## Run Cura with GUI (desktop mode).
def runWithGUI(self):
self._use_gui = True
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))
controller = self.getController()
# Initialize UI state
controller.setActiveStage("PrepareStage")
controller.setActiveView("SolidView")
controller.setCameraTool("CameraTool")
@ -667,67 +768,44 @@ class CuraApplication(QtApplication):
Selection.selectionChanged.connect(self.onSelectionChanged)
root = controller.getScene().getRoot()
# The platform is a child of BuildVolume
self._volume = BuildVolume.BuildVolume(root)
# Set the build volume of the arranger to the used build volume
Arrange.build_volume = self._volume
# Set default background color for scene
self.getRenderer().setBackgroundColor(QColor(245, 245, 245))
# Initialize platform physics
self._physics = PlatformPhysics.PlatformPhysics(controller, self._volume)
# Initialize camera
root = controller.getScene().getRoot()
camera = Camera("3d", root)
camera.setPosition(Vector(-80, 250, 700))
camera.setPerspective(True)
camera.lookAt(Vector(0, 0, 0))
controller.getScene().setActiveCamera("3d")
camera_tool = self.getController().getTool("CameraTool")
# Initialize camera tool
camera_tool = controller.getTool("CameraTool")
camera_tool.setOrigin(Vector(0, 100, 0))
camera_tool.setZoomRange(0.1, 200000)
# Initialize camera animations
self._camera_animation = CameraAnimation.CameraAnimation()
self._camera_animation.setCameraTool(self.getController().getTool("CameraTool"))
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading interface..."))
qmlRegisterSingletonType(ExtruderManager, "Cura", 1, 0, "ExtruderManager", self.getExtruderManager)
qmlRegisterSingletonType(MachineManager, "Cura", 1, 0, "MachineManager", self.getMachineManager)
qmlRegisterSingletonType(MaterialManager, "Cura", 1, 0, "MaterialManager", self.getMaterialManager)
qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, "SettingInheritanceManager",
self.getSettingInheritanceManager)
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 2, "SimpleModeSettingsManager",
self.getSimpleModeSettingsManager)
qmlRegisterSingletonType(ObjectsModel, "Cura", 1, 2, "ObjectsModel", self.getObjectsModel)
qmlRegisterSingletonType(BuildPlateModel, "Cura", 1, 2, "BuildPlateModel", self.getBuildPlateModel)
qmlRegisterSingletonType(CuraSceneController, "Cura", 1, 2, "SceneController", self.getCuraSceneController)
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
# Initialize QML engine
self.setMainQml(Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml"))
self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles))
run_without_gui = self.getCommandLineOption("headless", False)
if not run_without_gui:
self.initializeEngine()
# Make sure the correct stage is activated after QML is loaded
controller.setActiveStage("PrepareStage")
if run_without_gui or self._engine.rootObjects:
# Hide the splash screen
self.closeSplash()
for file_name in self.getCommandLineOption("file", []):
self._openFile(file_name)
for file_name in self._open_file_queue: #Open all the files that were queued up while plug-ins were loading.
self._openFile(file_name)
self._started = True
self.exec_()
def hasGui(self):
return self._use_gui
def getMachineManager(self, *args) -> MachineManager:
if self._machine_manager is None:
@ -795,15 +873,26 @@ class CuraApplication(QtApplication):
# \param engine The QML engine.
def registerObjects(self, engine):
super().registerObjects(engine)
# global contexts
engine.rootContext().setContextProperty("Printer", self)
engine.rootContext().setContextProperty("CuraApplication", self)
self._print_information = PrintInformation.PrintInformation()
engine.rootContext().setContextProperty("PrintInformation", self._print_information)
self._cura_actions = CuraActions.CuraActions(self)
engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
qmlRegisterSingletonType(CuraSceneController, "Cura", 1, 2, "SceneController", self.getCuraSceneController)
qmlRegisterSingletonType(ExtruderManager, "Cura", 1, 0, "ExtruderManager", self.getExtruderManager)
qmlRegisterSingletonType(MachineManager, "Cura", 1, 0, "MachineManager", self.getMachineManager)
qmlRegisterSingletonType(MaterialManager, "Cura", 1, 0, "MaterialManager", self.getMaterialManager)
qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, "SettingInheritanceManager", self.getSettingInheritanceManager)
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 2, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
qmlRegisterSingletonType(ObjectsModel, "Cura", 1, 2, "ObjectsModel", self.getObjectsModel)
qmlRegisterSingletonType(BuildPlateModel, "Cura", 1, 2, "BuildPlateModel", self.getBuildPlateModel)
qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
qmlRegisterType(ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel")
qmlRegisterSingletonType(ProfilesModel, "Cura", 1, 0, "ProfilesModel", ProfilesModel.createProfilesModel)
@ -890,12 +979,18 @@ class CuraApplication(QtApplication):
def getSceneBoundingBoxString(self):
return self._i18n_catalog.i18nc("@info 'width', 'depth' and 'height' are variable names that must NOT be translated; just translate the format of ##x##x## mm.", "%(width).1f x %(depth).1f x %(height).1f mm") % {'width' : self._scene_bounding_box.width.item(), 'depth': self._scene_bounding_box.depth.item(), 'height' : self._scene_bounding_box.height.item()}
## Update scene bounding box for current build plate
def updatePlatformActivity(self, node = None):
count = 0
scene_bounding_box = None
is_block_slicing_node = False
active_build_plate = self.getBuildPlateModel().activeBuildPlate
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if not issubclass(type(node), SceneNode) or (not node.getMeshData() and not node.callDecoration("getLayerData")):
if (
not issubclass(type(node), CuraSceneNode) or
(not node.getMeshData() and not node.callDecoration("getLayerData")) or
(node.callDecoration("getBuildPlateNumber") != active_build_plate)):
continue
if node.callDecoration("isBlockSlicing"):
is_block_slicing_node = True
@ -915,7 +1010,7 @@ class CuraApplication(QtApplication):
if not scene_bounding_box:
scene_bounding_box = AxisAlignedBox.Null
if repr(self._scene_bounding_box) != repr(scene_bounding_box) and scene_bounding_box.isValid():
if repr(self._scene_bounding_box) != repr(scene_bounding_box):
self._scene_bounding_box = scene_bounding_box
self.sceneBoundingBoxChanged.emit()
@ -1012,7 +1107,7 @@ class CuraApplication(QtApplication):
Selection.clear()
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if not issubclass(type(node), SceneNode):
if not isinstance(node, SceneNode):
continue
if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group.
@ -1020,24 +1115,29 @@ class CuraApplication(QtApplication):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.isSelectable():
continue # i.e. node with layer data
if not node.callDecoration("isSliceable"):
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
continue # i.e. node with layer data
Selection.add(node)
## Delete all nodes containing mesh data in the scene.
# \param only_selectable. Set this to False to delete objects from all build plates
@pyqtSlot()
def deleteAll(self):
def deleteAll(self, only_selectable = True):
Logger.log("i", "Clearing scene")
if not self.getController().getToolsEnabled():
return
nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if type(node) not in {SceneNode, CuraSceneNode}:
if not isinstance(node, SceneNode):
continue
if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group.
if only_selectable and not node.isSelectable():
continue
if not node.callDecoration("isSliceable") and not node.callDecoration("getLayerData") and not node.callDecoration("isGroup"):
continue # Only remove nodes that are selectable.
if node.getParent() and node.getParent().callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
nodes.append(node)
@ -1047,18 +1147,19 @@ class CuraApplication(QtApplication):
for node in nodes:
op.addOperation(RemoveSceneNodeOperation(node))
# Reset the print information
self.getController().getScene().sceneChanged.emit(node)
op.push()
Selection.clear()
self.getCuraSceneController().setActiveBuildPlate(0) # Select first build plate
## Reset all translation on nodes with mesh data.
@pyqtSlot()
def resetAllTranslation(self):
Logger.log("i", "Resetting all scene translations")
nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if not issubclass(type(node), SceneNode):
if not isinstance(node, SceneNode):
continue
if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group.
@ -1086,13 +1187,13 @@ class CuraApplication(QtApplication):
Logger.log("i", "Resetting all scene transformations")
nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if not issubclass(type(node), SceneNode):
if not isinstance(node, SceneNode):
continue
if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group.
if node.getParent() and node.getParent().callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.callDecoration("isSliceable"):
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
continue # i.e. node with layer data
nodes.append(node)
@ -1113,13 +1214,13 @@ class CuraApplication(QtApplication):
def arrangeObjectsToAllBuildPlates(self):
nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if not issubclass(type(node), SceneNode):
if not isinstance(node, SceneNode):
continue
if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group.
if node.getParent() and node.getParent().callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.callDecoration("isSliceable"):
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
continue # i.e. node with layer data
# Skip nodes that are too big
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
@ -1134,7 +1235,7 @@ class CuraApplication(QtApplication):
nodes = []
active_build_plate = self.getBuildPlateModel().activeBuildPlate
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if not issubclass(type(node), SceneNode):
if not isinstance(node, SceneNode):
continue
if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group.
@ -1142,7 +1243,7 @@ class CuraApplication(QtApplication):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.isSelectable():
continue # i.e. node with layer data
if not node.callDecoration("isSliceable"):
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
continue # i.e. node with layer data
if node.callDecoration("getBuildPlateNumber") == active_build_plate:
# Skip nodes that are too big
@ -1158,7 +1259,7 @@ class CuraApplication(QtApplication):
# What nodes are on the build plate and are not being moved
fixed_nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if not issubclass(type(node), SceneNode):
if not isinstance(node, SceneNode):
continue
if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group.
@ -1166,7 +1267,7 @@ class CuraApplication(QtApplication):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.isSelectable():
continue # i.e. node with layer data
if not node.callDecoration("isSliceable"):
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
continue # i.e. node with layer data
if node in nodes: # exclude selected node from fixed_nodes
continue
@ -1186,7 +1287,7 @@ class CuraApplication(QtApplication):
Logger.log("i", "Reloading all loaded mesh data.")
nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if not issubclass(type(node), SceneNode) or not node.getMeshData():
if not isinstance(node, SceneNode) or not node.getMeshData():
continue
nodes.append(node)
@ -1326,6 +1427,7 @@ class CuraApplication(QtApplication):
pass
fileLoaded = pyqtSignal(str)
fileCompleted = pyqtSignal(str)
def _reloadMeshFinished(self, job):
# TODO; This needs to be fixed properly. We now make the assumption that we only load a single mesh!
@ -1410,7 +1512,7 @@ class CuraApplication(QtApplication):
self._currently_loading_files.append(f)
if extension in self._non_sliceable_extensions:
self.deleteAll()
self.deleteAll(only_selectable = False)
job = ReadMeshJob(f)
job.finished.connect(self._readMeshFinished)
@ -1421,30 +1523,33 @@ class CuraApplication(QtApplication):
filename = job.getFileName()
self._currently_loading_files.remove(filename)
root = self.getController().getScene().getRoot()
arranger = Arrange.create(scene_root = root)
min_offset = 8
self.fileLoaded.emit(filename)
arrange_objects_on_load = (
not Preferences.getInstance().getValue("cura/use_multi_build_plate") or
Preferences.getInstance().getValue("cura/arrange_objects_on_load"))
not Preferences.getInstance().getValue("cura/not_arrange_objects_on_load"))
target_build_plate = self.getBuildPlateModel().activeBuildPlate if arrange_objects_on_load else -1
root = self.getController().getScene().getRoot()
fixed_nodes = []
for node_ in DepthFirstIterator(root):
if node_.callDecoration("isSliceable") and node_.callDecoration("getBuildPlateNumber") == target_build_plate:
fixed_nodes.append(node_)
arranger = Arrange.create(fixed_nodes = fixed_nodes)
min_offset = 8
for original_node in nodes:
node = CuraSceneNode() # We want our own CuraSceneNode
# Create a CuraSceneNode just if the original node is not that type
node = original_node if isinstance(original_node, CuraSceneNode) else CuraSceneNode()
node.setMeshData(original_node.getMeshData())
node.setSelectable(True)
node.setName(os.path.basename(filename))
self.getBuildVolume().checkBoundsAndUpdate(node)
extension = os.path.splitext(filename)[1]
if extension.lower() in self._non_sliceable_extensions:
self.getController().setActiveView("SimulationView")
view = self.getController().getActiveView()
view.resetLayerData()
view.setLayer(9999999)
view.calculateMaxLayers()
self.callLater(lambda: self.getController().setActiveView("SimulationView"))
block_slicing_decorator = BlockSlicingDecorator()
node.addDecorator(block_slicing_decorator)
@ -1477,12 +1582,21 @@ class CuraApplication(QtApplication):
# Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher
node, _ = arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10)
node.addDecorator(BuildPlateDecorator(target_build_plate))
# This node is deep copied from some other node which already has a BuildPlateDecorator, but the deepcopy
# of BuildPlateDecorator produces one that's associated with build plate -1. So, here we need to check if
# the BuildPlateDecorator exists or not and always set the correct build plate number.
build_plate_decorator = node.getDecorator(BuildPlateDecorator)
if build_plate_decorator is None:
build_plate_decorator = BuildPlateDecorator(target_build_plate)
node.addDecorator(build_plate_decorator)
build_plate_decorator.setBuildPlateNumber(target_build_plate)
op = AddSceneNodeOperation(node, scene.getRoot())
op.push()
scene.sceneChanged.emit(node)
self.fileCompleted.emit(filename)
def addNonSliceableExtension(self, extension):
self._non_sliceable_extensions.append(extension)
@ -1491,12 +1605,11 @@ class CuraApplication(QtApplication):
"""
Checks if the given file URL is a valid project file.
"""
try:
file_path = QUrl(file_url).toLocalFile()
workspace_reader = self.getWorkspaceFileHandler().getReaderForFile(file_path)
if workspace_reader is None:
return False # non-project files won't get a reader
try:
result = workspace_reader.preRead(file_path, show_dialog=False)
return result == WorkspaceReader.PreReadResult.accepted
except Exception as e:

View file

@ -4,6 +4,9 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection
from UM.Preferences import Preferences
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
## Keep track of all objects in the project
@ -24,18 +27,35 @@ class ObjectsModel(ListModel):
nodes = []
filter_current_build_plate = Preferences.getInstance().getValue("view/filter_current_build_plate")
active_build_plate_number = self._build_plate_number
group_nr = 1
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
if not issubclass(type(node), SceneNode) or (not node.getMeshData() and not node.callDecoration("getLayerData")):
if not isinstance(node, SceneNode):
continue
if not node.callDecoration("isSliceable"):
if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
continue
if node.getParent() and node.getParent().callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
continue
node_build_plate_number = node.callDecoration("getBuildPlateNumber")
if filter_current_build_plate and node_build_plate_number != active_build_plate_number:
continue
if not node.callDecoration("isGroup"):
name = node.getName()
else:
name = catalog.i18nc("@label", "Group #{group_nr}").format(group_nr = str(group_nr))
group_nr += 1
if hasattr(node, "isOutsideBuildArea"):
is_outside_build_area = node.isOutsideBuildArea()
else:
is_outside_build_area = False
nodes.append({
"name": node.getName(),
"name": name,
"isSelected": Selection.isSelected(node),
"isOutsideBuildArea": node.isOutsideBuildArea(),
"isOutsideBuildArea": is_outside_build_area,
"buildPlateNumber": node_build_plate_number,
"node": node
})

View file

@ -18,12 +18,13 @@ class OneAtATimeIterator(Iterator.Iterator):
def _fillStack(self):
node_list = []
for node in self._scene_node.getChildren():
if not type(node) is SceneNode:
if not issubclass(type(node), SceneNode):
continue
if node.callDecoration("getConvexHull"):
node_list.append(node)
if len(node_list) < 2:
self._node_stack = node_list[:]
return

View file

@ -8,7 +8,9 @@ from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
## Simple operation to set the buildplate number of a scenenode.
class SetBuildPlateNumberOperation(Operation):
def __init__(self, node: SceneNode, build_plate_nr: int) -> None:
super().__init__()
self._node = node
self._build_plate_nr = build_plate_nr
self._previous_build_plate_nr = None

View file

@ -34,7 +34,7 @@ class PlatformPhysics:
self._change_timer.timeout.connect(self._onChangeTimerFinished)
self._move_factor = 1.1 # By how much should we multiply overlap to calculate a new spot?
self._max_overlap_checks = 10 # How many times should we try to find a new spot per tick?
self._minimum_gap = 2 # It is a minimum distance between two models, applicable for small models
self._minimum_gap = 2 # It is a minimum distance (in mm) between two models, applicable for small models
Preferences.getInstance().addPreference("physics/automatic_push_free", True)
Preferences.getInstance().addPreference("physics/automatic_drop_down", True)
@ -42,7 +42,7 @@ class PlatformPhysics:
def _onSceneChanged(self, source):
self._change_timer.start()
def _onChangeTimerFinished(self, was_triggered_by_tool=False):
def _onChangeTimerFinished(self):
if not self._enabled:
return
@ -58,11 +58,10 @@ class PlatformPhysics:
# Only check nodes inside build area.
nodes = [node for node in nodes if (hasattr(node, "_outside_buildarea") and not node._outside_buildarea)]
active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
random.shuffle(nodes)
for node in nodes:
if node is root or not issubclass(type(node), SceneNode) or node.getBoundingBox() is None:
if node is root or not isinstance(node, SceneNode) or node.getBoundingBox() is None:
continue
bbox = node.getBoundingBox()
@ -135,13 +134,10 @@ class PlatformPhysics:
# if the distance between two models less than 2mm then try to find a new factor
if abs(temp_move_vector.x - overlap[0]) < self._minimum_gap and abs(temp_move_vector.y - overlap[1]) < self._minimum_gap:
temp_scale_factor = self._move_factor
temp_x_factor = (abs(overlap[0]) + self._minimum_gap) / overlap[0] if overlap[0] != 0 else 0 # find x move_factor, like (3.4 + 2) / 3.4 = 1.58
temp_y_factor = (abs(overlap[1]) + self._minimum_gap) / overlap[1] if overlap[1] != 0 else 0 # find y move_factor
if abs(temp_x_factor) > abs(temp_y_factor):
temp_scale_factor = temp_x_factor
else:
temp_scale_factor = temp_y_factor
temp_scale_factor = temp_x_factor if abs(temp_x_factor) > abs(temp_y_factor) else temp_y_factor
move_vector = move_vector.set(x = move_vector.x + overlap[0] * temp_scale_factor,
z = move_vector.z + overlap[1] * temp_scale_factor)
@ -181,4 +177,4 @@ class PlatformPhysics:
node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
self._enabled = True
self._onChangeTimerFinished(True)
self._onChangeTimerFinished()

View file

@ -1,7 +1,7 @@
# Copyright (c) 2017 Ultimaker B.V.
# Uranium is released under the terms of the LGPLv3 or higher.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from UM.Math.Color import Color
from UM.Resources import Resources
from UM.View.RenderPass import RenderPass
@ -39,7 +39,11 @@ class PreviewPass(RenderPass):
def render(self) -> None:
if not self._shader:
self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "object.shader"))
self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "overhang.shader"))
self._shader.setUniformValue("u_overhangAngle", 1.0)
self._gl.glClearColor(0.0, 0.0, 0.0, 0.0)
self._gl.glClear(self._gl.GL_COLOR_BUFFER_BIT | self._gl.GL_DEPTH_BUFFER_BIT)
# Create a new batch to be rendered
batch = RenderBatch(self._shader)
@ -47,7 +51,9 @@ class PreviewPass(RenderPass):
# Fill up the batch with objects that can be sliced. `
for node in DepthFirstIterator(self._scene.getRoot()):
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
batch.addItem(node.getWorldTransformation(), node.getMeshData())
uniforms = {}
uniforms["diffuse_color"] = node.getDiffuseColor()
batch.addItem(node.getWorldTransformation(), node.getMeshData(), uniforms = uniforms)
self.bind()
if self._camera is None:
@ -55,3 +61,4 @@ class PreviewPass(RenderPass):
else:
batch.render(self._camera)
self.release()

View file

@ -1,4 +1,4 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty
@ -8,9 +8,12 @@ from UM.Application import Application
from UM.Logger import Logger
from UM.Qt.Duration import Duration
from UM.Preferences import Preferences
from UM.Scene.SceneNode import SceneNode
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Settings.ExtruderManager import ExtruderManager
from typing import Dict
import math
import os.path
@ -64,6 +67,7 @@ class PrintInformation(QObject):
self._backend = Application.getInstance().getBackend()
if self._backend:
self._backend.printDurationMessage.connect(self._onPrintDurationMessage)
Application.getInstance().getController().getScene().sceneChanged.connect(self._onSceneChanged)
self._base_name = ""
self._abbr_machine = ""
@ -106,7 +110,6 @@ class PrintInformation(QObject):
self._print_time_message_values = {}
def _initPrintTimeMessageValues(self, build_plate_number):
# Full fill message values using keys from _print_time_message_translations
self._print_time_message_values[build_plate_number] = {}
@ -170,14 +173,14 @@ class PrintInformation(QObject):
def printTimes(self):
return self._print_time_message_values[self._active_build_plate]
def _onPrintDurationMessage(self, build_plate_number, print_time, material_amounts):
def _onPrintDurationMessage(self, build_plate_number, print_time: Dict[str, int], material_amounts: list):
self._updateTotalPrintTimePerFeature(build_plate_number, print_time)
self.currentPrintTimeChanged.emit()
self._material_amounts = material_amounts
self._calculateInformation(build_plate_number)
def _updateTotalPrintTimePerFeature(self, build_plate_number, print_time):
def _updateTotalPrintTimePerFeature(self, build_plate_number, print_time: Dict[str, int]):
total_estimated_time = 0
if build_plate_number not in self._print_time_message_values:
@ -358,10 +361,10 @@ class PrintInformation(QObject):
if not global_container_stack:
self._abbr_machine = ""
return
active_machine_type_id = global_container_stack.definition.getId()
global_stack_name = global_container_stack.getName()
abbr_machine = ""
for word in re.findall(r"[\w']+", global_stack_name):
for word in re.findall(r"[\w']+", active_machine_type_id):
if word.lower() == "ultimaker":
abbr_machine += "UM"
elif word.isdigit():
@ -393,12 +396,25 @@ class PrintInformation(QObject):
return result
# Simulate message with zero time duration
def setToZeroPrintInformation(self, build_plate_number):
temp_message = {}
if build_plate_number not in self._print_time_message_values:
self._print_time_message_values[build_plate_number] = {}
for key in self._print_time_message_values[build_plate_number].keys():
temp_message[key] = 0
def setToZeroPrintInformation(self, build_plate):
# Construct the 0-time message
temp_message = {}
if build_plate not in self._print_time_message_values:
self._print_time_message_values[build_plate] = {}
for key in self._print_time_message_values[build_plate].keys():
temp_message[key] = 0
temp_material_amounts = [0]
self._onPrintDurationMessage(build_plate_number, temp_message, temp_material_amounts)
self._onPrintDurationMessage(build_plate, temp_message, temp_material_amounts)
## Listen to scene changes to check if we need to reset the print information
def _onSceneChanged(self, scene_node):
# Ignore any changes that are not related to sliceable objects
if not isinstance(scene_node, SceneNode)\
or not scene_node.callDecoration("isSliceable")\
or not scene_node.callDecoration("getBuildPlateNumber") == self._active_build_plate:
return
self.setToZeroPrintInformation(self._active_build_plate)

View file

@ -0,0 +1,70 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot
from UM.Logger import Logger
from typing import Optional
MYPY = False
if MYPY:
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
class ExtruderOutputModel(QObject):
hotendIDChanged = pyqtSignal()
targetHotendTemperatureChanged = pyqtSignal()
hotendTemperatureChanged = pyqtSignal()
activeMaterialChanged = pyqtSignal()
def __init__(self, printer: "PrinterOutputModel", parent=None):
super().__init__(parent)
self._printer = printer
self._target_hotend_temperature = 0
self._hotend_temperature = 0
self._hotend_id = ""
self._active_material = None # type: Optional[MaterialOutputModel]
@pyqtProperty(QObject, notify = activeMaterialChanged)
def activeMaterial(self) -> "MaterialOutputModel":
return self._active_material
def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]):
if self._active_material != material:
self._active_material = material
self.activeMaterialChanged.emit()
## Update the hotend temperature. This only changes it locally.
def updateHotendTemperature(self, temperature: float):
if self._hotend_temperature != temperature:
self._hotend_temperature = temperature
self.hotendTemperatureChanged.emit()
def updateTargetHotendTemperature(self, temperature: float):
if self._target_hotend_temperature != temperature:
self._target_hotend_temperature = temperature
self.targetHotendTemperatureChanged.emit()
## Set the target hotend temperature. This ensures that it's actually sent to the remote.
@pyqtSlot(float)
def setTargetHotendTemperature(self, temperature: float):
self._printer.getController().setTargetHotendTemperature(self._printer, self, temperature)
self.updateTargetHotendTemperature(temperature)
@pyqtProperty(float, notify = targetHotendTemperatureChanged)
def targetHotendTemperature(self) -> float:
return self._target_hotend_temperature
@pyqtProperty(float, notify=hotendTemperatureChanged)
def hotendTemperature(self) -> float:
return self._hotend_temperature
@pyqtProperty(str, notify = hotendIDChanged)
def hotendID(self) -> str:
return self._hotend_id
def updateHotendID(self, id: str):
if self._hotend_id != id:
self._hotend_id = id
self.hotendIDChanged.emit()

View file

@ -0,0 +1,34 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot
class MaterialOutputModel(QObject):
def __init__(self, guid, type, color, brand, name, parent = None):
super().__init__(parent)
self._guid = guid
self._type = type
self._color = color
self._brand = brand
self._name = name
@pyqtProperty(str, constant = True)
def guid(self):
return self._guid
@pyqtProperty(str, constant=True)
def type(self):
return self._type
@pyqtProperty(str, constant=True)
def brand(self):
return self._brand
@pyqtProperty(str, constant=True)
def color(self):
return self._color
@pyqtProperty(str, constant=True)
def name(self):
return self._name

View file

@ -0,0 +1,119 @@
from UM.Logger import Logger
from PyQt5.QtCore import QUrl, pyqtProperty, pyqtSignal, QObject, pyqtSlot
from PyQt5.QtGui import QImage
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
class NetworkCamera(QObject):
newImage = pyqtSignal()
def __init__(self, target = None, parent = None):
super().__init__(parent)
self._stream_buffer = b""
self._stream_buffer_start_index = -1
self._manager = None
self._image_request = None
self._image_reply = None
self._image = QImage()
self._image_id = 0
self._target = target
self._started = False
@pyqtSlot(str)
def setTarget(self, target):
restart_required = False
if self._started:
self.stop()
restart_required = True
self._target = target
if restart_required:
self.start()
@pyqtProperty(QUrl, notify=newImage)
def latestImage(self):
self._image_id += 1
# There is an image provider that is called "camera". In order to ensure that the image qml object, that
# requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
# as new (instead of relying on cached version and thus forces an update.
temp = "image://camera/" + str(self._image_id)
return QUrl(temp, QUrl.TolerantMode)
@pyqtSlot()
def start(self):
# Ensure that previous requests (if any) are stopped.
self.stop()
if self._target is None:
Logger.log("w", "Unable to start camera stream without target!")
return
self._started = True
url = QUrl(self._target)
self._image_request = QNetworkRequest(url)
if self._manager is None:
self._manager = QNetworkAccessManager()
self._image_reply = self._manager.get(self._image_request)
self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress)
@pyqtSlot()
def stop(self):
self._stream_buffer = b""
self._stream_buffer_start_index = -1
if self._image_reply:
try:
# disconnect the signal
try:
self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress)
except Exception:
pass
# abort the request if it's not finished
if not self._image_reply.isFinished():
self._image_reply.close()
except Exception as e: # RuntimeError
pass # It can happen that the wrapped c++ object is already deleted.
self._image_reply = None
self._image_request = None
self._manager = None
self._started = False
def getImage(self):
return self._image
## Ensure that close gets called when object is destroyed
def __del__(self):
self.stop()
def _onStreamDownloadProgress(self, bytes_received, bytes_total):
# An MJPG stream is (for our purpose) a stream of concatenated JPG images.
# JPG images start with the marker 0xFFD8, and end with 0xFFD9
if self._image_reply is None:
return
self._stream_buffer += self._image_reply.readAll()
if len(self._stream_buffer) > 2000000: # No single camera frame should be 2 Mb or larger
Logger.log("w", "MJPEG buffer exceeds reasonable size. Restarting stream...")
self.stop() # resets stream buffer and start index
self.start()
return
if self._stream_buffer_start_index == -1:
self._stream_buffer_start_index = self._stream_buffer.indexOf(b'\xff\xd8')
stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9')
# If this happens to be more than a single frame, then so be it; the JPG decoder will
# ignore the extra data. We do it like this in order not to get a buildup of frames
if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1:
jpg_data = self._stream_buffer[self._stream_buffer_start_index:stream_buffer_end_index + 2]
self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:]
self._stream_buffer_start_index = -1
self._image.loadFromData(jpg_data)
self.newImage.emit()

View file

@ -0,0 +1,304 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from UM.Logger import Logger
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, pyqtSignal, QUrl, QCoreApplication
from time import time
from typing import Callable, Any, Optional, Dict, Tuple
from enum import IntEnum
from typing import List
import os # To get the username
import gzip
class AuthState(IntEnum):
NotAuthenticated = 1
AuthenticationRequested = 2
Authenticated = 3
AuthenticationDenied = 4
AuthenticationReceived = 5
class NetworkedPrinterOutputDevice(PrinterOutputDevice):
authenticationStateChanged = pyqtSignal()
def __init__(self, device_id, address: str, properties, parent = None) -> None:
super().__init__(device_id = device_id, parent = parent)
self._manager = None # type: QNetworkAccessManager
self._last_manager_create_time = None # type: float
self._recreate_network_manager_time = 30
self._timeout_time = 10 # After how many seconds of no response should a timeout occur?
self._last_response_time = None # type: float
self._last_request_time = None # type: float
self._api_prefix = ""
self._address = address
self._properties = properties
self._user_agent = "%s/%s " % (Application.getInstance().getApplicationName(), Application.getInstance().getVersion())
self._onFinishedCallbacks = {} # type: Dict[str, Callable[[QNetworkReply], None]]
self._authentication_state = AuthState.NotAuthenticated
# QHttpMultiPart objects need to be kept alive and not garbage collected during the
# HTTP which uses them. We hold references to these QHttpMultiPart objects here.
self._kept_alive_multiparts = {} # type: Dict[QNetworkReply, QHttpMultiPart]
self._sending_gcode = False
self._compressing_gcode = False
self._gcode = [] # type: List[str]
self._connection_state_before_timeout = None # type: Optional[ConnectionState]
def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs) -> None:
raise NotImplementedError("requestWrite needs to be implemented")
def setAuthenticationState(self, authentication_state) -> None:
if self._authentication_state != authentication_state:
self._authentication_state = authentication_state
self.authenticationStateChanged.emit()
@pyqtProperty(int, notify=authenticationStateChanged)
def authenticationState(self) -> int:
return self._authentication_state
def _compressDataAndNotifyQt(self, data_to_append: str) -> bytes:
compressed_data = gzip.compress(data_to_append.encode("utf-8"))
self._progress_message.setProgress(-1) # Tickle the message so that it's clear that it's still being used.
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
# Pretend that this is a response, as zipping might take a bit of time.
# If we don't do this, the device might trigger a timeout.
self._last_response_time = time()
return compressed_data
def _compressGCode(self) -> Optional[bytes]:
self._compressing_gcode = True
## Mash the data into single string
max_chars_per_line = int(1024 * 1024 / 4) # 1/4 MB per line.
file_data_bytes_list = []
batched_lines = []
batched_lines_count = 0
for line in self._gcode:
if not self._compressing_gcode:
self._progress_message.hide()
# Stop trying to zip / send as abort was called.
return None
# if the gcode was read from a gcode file, self._gcode will be a list of all lines in that file.
# Compressing line by line in this case is extremely slow, so we need to batch them.
batched_lines.append(line)
batched_lines_count += len(line)
if batched_lines_count >= max_chars_per_line:
file_data_bytes_list.append(self._compressDataAndNotifyQt("".join(batched_lines)))
batched_lines = []
batched_lines_count = 0
# Don't miss the last batch (If any)
if len(batched_lines) != 0:
file_data_bytes_list.append(self._compressDataAndNotifyQt("".join(batched_lines)))
self._compressing_gcode = False
return b"".join(file_data_bytes_list)
def _update(self) -> bool:
if self._last_response_time:
time_since_last_response = time() - self._last_response_time
else:
time_since_last_response = 0
if self._last_request_time:
time_since_last_request = time() - self._last_request_time
else:
time_since_last_request = float("inf") # An irrelevantly large number of seconds
if time_since_last_response > self._timeout_time >= time_since_last_request:
# Go (or stay) into timeout.
if self._connection_state_before_timeout is None:
self._connection_state_before_timeout = self._connection_state
self.setConnectionState(ConnectionState.closed)
# We need to check if the manager needs to be re-created. If we don't, we get some issues when OSX goes to
# sleep.
if time_since_last_response > self._recreate_network_manager_time:
if self._last_manager_create_time is None:
self._createNetworkManager()
if time() - self._last_manager_create_time > self._recreate_network_manager_time:
self._createNetworkManager()
elif self._connection_state == ConnectionState.closed:
# Go out of timeout.
self.setConnectionState(self._connection_state_before_timeout)
self._connection_state_before_timeout = None
return True
def _createEmptyRequest(self, target, content_type: Optional[str] = "application/json") -> QNetworkRequest:
url = QUrl("http://" + self._address + self._api_prefix + target)
request = QNetworkRequest(url)
if content_type is not None:
request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent)
return request
def _createFormPart(self, content_header, data, content_type = None) -> QHttpPart:
part = QHttpPart()
if not content_header.startswith("form-data;"):
content_header = "form_data; " + content_header
part.setHeader(QNetworkRequest.ContentDispositionHeader, content_header)
if content_type is not None:
part.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
part.setBody(data)
return part
## Convenience function to get the username from the OS.
# The code was copied from the getpass module, as we try to use as little dependencies as possible.
def _getUserName(self) -> str:
for name in ("LOGNAME", "USER", "LNAME", "USERNAME"):
user = os.environ.get(name)
if user:
return user
return "Unknown User" # Couldn't find out username.
def _clearCachedMultiPart(self, reply: QNetworkReply) -> None:
if reply in self._kept_alive_multiparts:
del self._kept_alive_multiparts[reply]
def put(self, target: str, data: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None:
if self._manager is None:
self._createNetworkManager()
request = self._createEmptyRequest(target)
self._last_request_time = time()
reply = self._manager.put(request, data.encode())
self._registerOnFinishedCallback(reply, onFinished)
def get(self, target: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None:
if self._manager is None:
self._createNetworkManager()
request = self._createEmptyRequest(target)
self._last_request_time = time()
reply = self._manager.get(request)
self._registerOnFinishedCallback(reply, onFinished)
def post(self, target: str, data: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None:
if self._manager is None:
self._createNetworkManager()
request = self._createEmptyRequest(target)
self._last_request_time = time()
reply = self._manager.post(request, data)
if onProgress is not None:
reply.uploadProgress.connect(onProgress)
self._registerOnFinishedCallback(reply, onFinished)
def postFormWithParts(self, target:str, parts: List[QHttpPart], onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None:
if self._manager is None:
self._createNetworkManager()
request = self._createEmptyRequest(target, content_type=None)
multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType)
for part in parts:
multi_post_part.append(part)
self._last_request_time = time()
reply = self._manager.post(request, multi_post_part)
self._kept_alive_multiparts[reply] = multi_post_part
if onProgress is not None:
reply.uploadProgress.connect(onProgress)
self._registerOnFinishedCallback(reply, onFinished)
def postForm(self, target: str, header_data: str, body_data: bytes, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None:
post_part = QHttpPart()
post_part.setHeader(QNetworkRequest.ContentDispositionHeader, header_data)
post_part.setBody(body_data)
self.postFormWithParts(target, [post_part], onFinished, onProgress)
def _onAuthenticationRequired(self, reply, authenticator) -> None:
Logger.log("w", "Request to {url} required authentication, which was not implemented".format(url = reply.url().toString()))
def _createNetworkManager(self) -> None:
Logger.log("d", "Creating network manager")
if self._manager:
self._manager.finished.disconnect(self.__handleOnFinished)
self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired)
self._manager = QNetworkAccessManager()
self._manager.finished.connect(self.__handleOnFinished)
self._last_manager_create_time = time()
self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
def _registerOnFinishedCallback(self, reply: QNetworkReply, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None:
if onFinished is not None:
self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished
def __handleOnFinished(self, reply: QNetworkReply) -> None:
# Due to garbage collection, we need to cache certain bits of post operations.
# As we don't want to keep them around forever, delete them if we get a reply.
if reply.operation() == QNetworkAccessManager.PostOperation:
self._clearCachedMultiPart(reply)
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) is None:
# No status code means it never even reached remote.
return
self._last_response_time = time()
if self._connection_state == ConnectionState.connecting:
self.setConnectionState(ConnectionState.connected)
callback_key = reply.url().toString() + str(reply.operation())
try:
if callback_key in self._onFinishedCallbacks:
self._onFinishedCallbacks[callback_key](reply)
except Exception:
Logger.logException("w", "something went wrong with callback")
@pyqtSlot(str, result=str)
def getProperty(self, key: str) -> str:
bytes_key = key.encode("utf-8")
if bytes_key in self._properties:
return self._properties.get(bytes_key, b"").decode("utf-8")
else:
return ""
def getProperties(self):
return self._properties
## Get the unique key of this machine
# \return key String containing the key of the machine.
@pyqtProperty(str, constant=True)
def key(self) -> str:
return self._id
## The IP address of the printer.
@pyqtProperty(str, constant=True)
def address(self) -> str:
return self._properties.get(b"address", b"").decode("utf-8")
## Name of the printer (as returned from the ZeroConf properties)
@pyqtProperty(str, constant=True)
def name(self) -> str:
return self._properties.get(b"name", b"").decode("utf-8")
## Firmware version (as returned from the ZeroConf properties)
@pyqtProperty(str, constant=True)
def firmwareVersion(self) -> str:
return self._properties.get(b"firmware_version", b"").decode("utf-8")
## IPadress of this printer
@pyqtProperty(str, constant=True)
def ipAddress(self) -> str:
return self._address

View file

@ -0,0 +1,101 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
from typing import Optional
MYPY = False
if MYPY:
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
class PrintJobOutputModel(QObject):
stateChanged = pyqtSignal()
timeTotalChanged = pyqtSignal()
timeElapsedChanged = pyqtSignal()
nameChanged = pyqtSignal()
keyChanged = pyqtSignal()
assignedPrinterChanged = pyqtSignal()
ownerChanged = pyqtSignal()
def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent=None):
super().__init__(parent)
self._output_controller = output_controller
self._state = ""
self._time_total = 0
self._time_elapsed = 0
self._name = name # Human readable name
self._key = key # Unique identifier
self._assigned_printer = None # type: Optional[PrinterOutputModel]
self._owner = "" # Who started/owns the print job?
@pyqtProperty(str, notify=ownerChanged)
def owner(self):
return self._owner
def updateOwner(self, owner):
if self._owner != owner:
self._owner = owner
self.ownerChanged.emit()
@pyqtProperty(QObject, notify=assignedPrinterChanged)
def assignedPrinter(self):
return self._assigned_printer
def updateAssignedPrinter(self, assigned_printer: "PrinterOutputModel"):
if self._assigned_printer != assigned_printer:
old_printer = self._assigned_printer
self._assigned_printer = assigned_printer
if old_printer is not None:
# If the previously assigned printer is set, this job is moved away from it.
old_printer.updateActivePrintJob(None)
self.assignedPrinterChanged.emit()
@pyqtProperty(str, notify=keyChanged)
def key(self):
return self._key
def updateKey(self, key: str):
if self._key != key:
self._key = key
self.keyChanged.emit()
@pyqtProperty(str, notify = nameChanged)
def name(self):
return self._name
def updateName(self, name: str):
if self._name != name:
self._name = name
self.nameChanged.emit()
@pyqtProperty(int, notify = timeTotalChanged)
def timeTotal(self):
return self._time_total
@pyqtProperty(int, notify = timeElapsedChanged)
def timeElapsed(self):
return self._time_elapsed
@pyqtProperty(str, notify=stateChanged)
def state(self):
return self._state
def updateTimeTotal(self, new_time_total):
if self._time_total != new_time_total:
self._time_total = new_time_total
self.timeTotalChanged.emit()
def updateTimeElapsed(self, new_time_elapsed):
if self._time_elapsed != new_time_elapsed:
self._time_elapsed = new_time_elapsed
self.timeElapsedChanged.emit()
def updateState(self, new_state):
if self._state != new_state:
self._state = new_state
self.stateChanged.emit()
@pyqtSlot(str)
def setState(self, state):
self._output_controller.setJobState(self, state)

View file

@ -0,0 +1,46 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger
MYPY = False
if MYPY:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.ExtruderOuputModel import ExtruderOuputModel
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
class PrinterOutputController:
def __init__(self, output_device):
self.can_pause = True
self.can_abort = True
self.can_pre_heat_bed = True
self.can_control_manually = True
self._output_device = output_device
def setTargetHotendTemperature(self, printer: "PrinterOutputModel", extruder: "ExtruderOuputModel", temperature: int):
Logger.log("w", "Set target hotend temperature not implemented in controller")
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int):
Logger.log("w", "Set target bed temperature not implemented in controller")
def setJobState(self, job: "PrintJobOutputModel", state: str):
Logger.log("w", "Set job state not implemented in controller")
def cancelPreheatBed(self, printer: "PrinterOutputModel"):
Logger.log("w", "Cancel preheat bed not implemented in controller")
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration):
Logger.log("w", "Preheat bed not implemented in controller")
def setHeadPosition(self, printer: "PrinterOutputModel", x, y, z, speed):
Logger.log("w", "Set head position not implemented in controller")
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed):
Logger.log("w", "Move head not implemented in controller")
def homeBed(self, printer):
Logger.log("w", "Home bed not implemented in controller")
def homeHead(self, printer):
Logger.log("w", "Home head not implemented in controller")

View file

@ -0,0 +1,240 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot
from UM.Logger import Logger
from typing import Optional, List
from UM.Math.Vector import Vector
from cura.PrinterOutput.ExtruderOuputModel import ExtruderOutputModel
MYPY = False
if MYPY:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
class PrinterOutputModel(QObject):
bedTemperatureChanged = pyqtSignal()
targetBedTemperatureChanged = pyqtSignal()
isPreheatingChanged = pyqtSignal()
stateChanged = pyqtSignal()
activePrintJobChanged = pyqtSignal()
nameChanged = pyqtSignal()
headPositionChanged = pyqtSignal()
keyChanged = pyqtSignal()
typeChanged = pyqtSignal()
cameraChanged = pyqtSignal()
def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = ""):
super().__init__(parent)
self._bed_temperature = -1 # Use -1 for no heated bed.
self._target_bed_temperature = 0
self._name = ""
self._key = "" # Unique identifier
self._controller = output_controller
self._extruders = [ExtruderOutputModel(printer=self) for i in range(number_of_extruders)]
self._head_position = Vector(0, 0, 0)
self._active_print_job = None # type: Optional[PrintJobOutputModel]
self._firmware_version = firmware_version
self._printer_state = "unknown"
self._is_preheating = False
self._type = ""
self._camera = None
@pyqtProperty(str, constant = True)
def firmwareVersion(self):
return self._firmware_version
def setCamera(self, camera):
if self._camera is not camera:
self._camera = camera
self.cameraChanged.emit()
def updateIsPreheating(self, pre_heating):
if self._is_preheating != pre_heating:
self._is_preheating = pre_heating
self.isPreheatingChanged.emit()
@pyqtProperty(bool, notify=isPreheatingChanged)
def isPreheating(self):
return self._is_preheating
@pyqtProperty(QObject, notify=cameraChanged)
def camera(self):
return self._camera
@pyqtProperty(str, notify = typeChanged)
def type(self):
return self._type
def updateType(self, type):
if self._type != type:
self._type = type
self.typeChanged.emit()
@pyqtProperty(str, notify=keyChanged)
def key(self):
return self._key
def updateKey(self, key: str):
if self._key != key:
self._key = key
self.keyChanged.emit()
@pyqtSlot()
def homeHead(self):
self._controller.homeHead(self)
@pyqtSlot()
def homeBed(self):
self._controller.homeBed(self)
@pyqtProperty("QVariantList", constant = True)
def extruders(self):
return self._extruders
@pyqtProperty(QVariant, notify = headPositionChanged)
def headPosition(self):
return {"x": self._head_position.x, "y": self._head_position.y, "z": self.head_position_z}
def updateHeadPosition(self, x, y, z):
if self._head_position.x != x or self._head_position.y != y or self._head_position.z != z:
self._head_position = Vector(x, y, z)
self.headPositionChanged.emit()
@pyqtProperty("long", "long", "long")
@pyqtProperty("long", "long", "long", "long")
def setHeadPosition(self, x, y, z, speed = 3000):
self.updateHeadPosition(x, y, z)
self._controller.setHeadPosition(self, x, y, z, speed)
@pyqtProperty("long")
@pyqtProperty("long", "long")
def setHeadX(self, x, speed = 3000):
self.updateHeadPosition(x, self._head_position.y, self._head_position.z)
self._controller.setHeadPosition(self, x, self._head_position.y, self._head_position.z, speed)
@pyqtProperty("long")
@pyqtProperty("long", "long")
def setHeadY(self, y, speed = 3000):
self.updateHeadPosition(self._head_position.x, y, self._head_position.z)
self._controller.setHeadPosition(self, self._head_position.x, y, self._head_position.z, speed)
@pyqtProperty("long")
@pyqtProperty("long", "long")
def setHeadZ(self, z, speed = 3000):
self.updateHeadPosition(self._head_position.x, self._head_position.y, z)
self._controller.setHeadPosition(self, self._head_position.x, self._head_position.y, z, speed)
@pyqtSlot("long", "long", "long")
@pyqtSlot("long", "long", "long", "long")
def moveHead(self, x = 0, y = 0, z = 0, speed = 3000):
self._controller.moveHead(self, x, y, z, speed)
## Pre-heats the heated bed of the printer.
#
# \param temperature The temperature to heat the bed to, in degrees
# Celsius.
# \param duration How long the bed should stay warm, in seconds.
@pyqtSlot(float, float)
def preheatBed(self, temperature, duration):
self._controller.preheatBed(self, temperature, duration)
@pyqtSlot()
def cancelPreheatBed(self):
self._controller.cancelPreheatBed(self)
def getController(self):
return self._controller
@pyqtProperty(str, notify=nameChanged)
def name(self):
return self._name
def setName(self, name):
self._setName(name)
self.updateName(name)
def updateName(self, name):
if self._name != name:
self._name = name
self.nameChanged.emit()
## Update the bed temperature. This only changes it locally.
def updateBedTemperature(self, temperature):
if self._bed_temperature != temperature:
self._bed_temperature = temperature
self.bedTemperatureChanged.emit()
def updateTargetBedTemperature(self, temperature):
if self._target_bed_temperature != temperature:
self._target_bed_temperature = temperature
self.targetBedTemperatureChanged.emit()
## Set the target bed temperature. This ensures that it's actually sent to the remote.
@pyqtSlot(int)
def setTargetBedTemperature(self, temperature):
self._controller.setTargetBedTemperature(self, temperature)
self.updateTargetBedTemperature(temperature)
def updateActivePrintJob(self, print_job):
if self._active_print_job != print_job:
old_print_job = self._active_print_job
if print_job is not None:
print_job.updateAssignedPrinter(self)
self._active_print_job = print_job
if old_print_job is not None:
old_print_job.updateAssignedPrinter(None)
self.activePrintJobChanged.emit()
def updateState(self, printer_state):
if self._printer_state != printer_state:
self._printer_state = printer_state
self.stateChanged.emit()
@pyqtProperty(QObject, notify = activePrintJobChanged)
def activePrintJob(self):
return self._active_print_job
@pyqtProperty(str, notify=stateChanged)
def state(self):
return self._printer_state
@pyqtProperty(int, notify = bedTemperatureChanged)
def bedTemperature(self):
return self._bed_temperature
@pyqtProperty(int, notify=targetBedTemperatureChanged)
def targetBedTemperature(self):
return self._target_bed_temperature
# Does the printer support pre-heating the bed at all
@pyqtProperty(bool, constant=True)
def canPreHeatBed(self):
if self._controller:
return self._controller.can_pre_heat_bed
return False
# Does the printer support pause at all
@pyqtProperty(bool, constant=True)
def canPause(self):
if self._controller:
return self._controller.can_pause
return False
# Does the printer support abort at all
@pyqtProperty(bool, constant=True)
def canAbort(self):
if self._controller:
return self._controller.can_abort
return False
# Does the printer support manual control at all
@pyqtProperty(bool, constant=True)
def canControlManually(self):
if self._controller:
return self._controller.can_control_manually
return False

View file

View file

@ -3,15 +3,21 @@
from UM.i18n import i18nCatalog
from UM.OutputDevice.OutputDevice import OutputDevice
from PyQt5.QtCore import pyqtProperty, pyqtSlot, QObject, QTimer, pyqtSignal
from PyQt5.QtCore import pyqtProperty, QObject, QTimer, pyqtSignal
from PyQt5.QtWidgets import QMessageBox
from enum import IntEnum # For the connection state tracking.
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Logger import Logger
from UM.Signal import signalemitter
from UM.Application import Application
from enum import IntEnum # For the connection state tracking.
from typing import List, Optional
MYPY = False
if MYPY:
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
i18n_catalog = i18nCatalog("cura")
## Printer output device adds extra interface options on top of output device.
@ -25,663 +31,150 @@ i18n_catalog = i18nCatalog("cura")
# For all other uses it should be used in the same way as a "regular" OutputDevice.
@signalemitter
class PrinterOutputDevice(QObject, OutputDevice):
printersChanged = pyqtSignal()
connectionStateChanged = pyqtSignal(str)
acceptsCommandsChanged = pyqtSignal()
# Signal to indicate that the material of the active printer on the remote changed.
materialIdChanged = pyqtSignal()
# # Signal to indicate that the hotend of the active printer on the remote changed.
hotendIdChanged = pyqtSignal()
# Signal to indicate that the info text about the connection has changed.
connectionTextChanged = pyqtSignal()
def __init__(self, device_id, parent = None):
super().__init__(device_id = device_id, parent = parent)
self._container_registry = ContainerRegistry.getInstance()
self._target_bed_temperature = 0
self._bed_temperature = 0
self._num_extruders = 1
self._hotend_temperatures = [0] * self._num_extruders
self._target_hotend_temperatures = [0] * self._num_extruders
self._material_ids = [""] * self._num_extruders
self._hotend_ids = [""] * self._num_extruders
self._progress = 0
self._head_x = 0
self._head_y = 0
self._head_z = 0
self._connection_state = ConnectionState.closed
self._connection_text = ""
self._time_elapsed = 0
self._time_total = 0
self._job_state = ""
self._job_name = ""
self._error_text = ""
self._accepts_commands = True
self._preheat_bed_timeout = 900 # Default time-out for pre-heating the bed, in seconds.
self._preheat_bed_timer = QTimer() # Timer that tracks how long to preheat still.
self._preheat_bed_timer.setSingleShot(True)
self._preheat_bed_timer.timeout.connect(self.cancelPreheatBed)
self._printer_state = ""
self._printer_type = "unknown"
self._camera_active = False
self._printers = [] # type: List[PrinterOutputModel]
self._monitor_view_qml_path = ""
self._monitor_component = None
self._monitor_item = None
self._control_view_qml_path = ""
self._control_component = None
self._control_item = None
self._qml_context = None
self._can_pause = True
self._can_abort = True
self._can_pre_heat_bed = True
self._can_control_manually = True
self._accepts_commands = False
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None):
self._update_timer = QTimer()
self._update_timer.setInterval(2000) # TODO; Add preference for update interval
self._update_timer.setSingleShot(False)
self._update_timer.timeout.connect(self._update)
self._connection_state = ConnectionState.closed
self._address = ""
self._connection_text = ""
@pyqtProperty(str, notify = connectionTextChanged)
def address(self):
return self._address
def setConnectionText(self, connection_text):
if self._connection_text != connection_text:
self._connection_text = connection_text
self.connectionTextChanged.emit()
@pyqtProperty(str, constant=True)
def connectionText(self):
return self._connection_text
def materialHotendChangedMessage(self, callback):
Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'")
callback(QMessageBox.Yes)
def isConnected(self):
return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error
def setConnectionState(self, connection_state):
if self._connection_state != connection_state:
self._connection_state = connection_state
self.connectionStateChanged.emit(self._id)
@pyqtProperty(str, notify = connectionStateChanged)
def connectionState(self):
return self._connection_state
def _update(self):
pass
def _getPrinterByKey(self, key) -> Optional["PrinterOutputModel"]:
for printer in self._printers:
if printer.key == key:
return printer
return None
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
raise NotImplementedError("requestWrite needs to be implemented")
## Signals
@pyqtProperty(QObject, notify = printersChanged)
def activePrinter(self) -> Optional["PrinterOutputModel"]:
if len(self._printers):
return self._printers[0]
return None
# Signal to be emitted when bed temp is changed
bedTemperatureChanged = pyqtSignal()
# Signal to be emitted when target bed temp is changed
targetBedTemperatureChanged = pyqtSignal()
# Signal when the progress is changed (usually when this output device is printing / sending lots of data)
progressChanged = pyqtSignal()
# Signal to be emitted when hotend temp is changed
hotendTemperaturesChanged = pyqtSignal()
# Signal to be emitted when target hotend temp is changed
targetHotendTemperaturesChanged = pyqtSignal()
# Signal to be emitted when head position is changed (x,y,z)
headPositionChanged = pyqtSignal()
# Signal to be emitted when either of the material ids is changed
materialIdChanged = pyqtSignal(int, str, arguments = ["index", "id"])
# Signal to be emitted when either of the hotend ids is changed
hotendIdChanged = pyqtSignal(int, str, arguments = ["index", "id"])
# Signal that is emitted every time connection state is changed.
# it also sends it's own device_id (for convenience sake)
connectionStateChanged = pyqtSignal(str)
connectionTextChanged = pyqtSignal()
timeElapsedChanged = pyqtSignal()
timeTotalChanged = pyqtSignal()
jobStateChanged = pyqtSignal()
jobNameChanged = pyqtSignal()
errorTextChanged = pyqtSignal()
acceptsCommandsChanged = pyqtSignal()
printerStateChanged = pyqtSignal()
printerTypeChanged = pyqtSignal()
# Signal to be emitted when some drastic change occurs in the remaining time (not when the time just passes on normally).
preheatBedRemainingTimeChanged = pyqtSignal()
# Does the printer support pre-heating the bed at all
@pyqtProperty(bool, constant=True)
def canPreHeatBed(self):
return self._can_pre_heat_bed
# Does the printer support pause at all
@pyqtProperty(bool, constant=True)
def canPause(self):
return self._can_pause
# Does the printer support abort at all
@pyqtProperty(bool, constant=True)
def canAbort(self):
return self._can_abort
# Does the printer support manual control at all
@pyqtProperty(bool, constant=True)
def canControlManually(self):
return self._can_control_manually
@pyqtProperty("QVariantList", notify = printersChanged)
def printers(self):
return self._printers
@pyqtProperty(QObject, constant=True)
def monitorItem(self):
# Note that we specifically only check if the monitor component is created.
# It could be that it failed to actually create the qml item! If we check if the item was created, it will try to
# create the item (and fail) every time.
if not self._monitor_item:
if not self._monitor_component:
self._createMonitorViewFromQML()
return self._monitor_item
@pyqtProperty(QObject, constant=True)
def controlItem(self):
if not self._control_item:
if not self._control_component:
self._createControlViewFromQML()
return self._control_item
def _createControlViewFromQML(self):
if not self._control_view_qml_path:
return
self._control_item = Application.getInstance().createQmlComponent(self._control_view_qml_path, {
"OutputDevice": self
})
if self._control_item is None:
self._control_item = Application.getInstance().createQmlComponent(self._control_view_qml_path, {"OutputDevice": self})
def _createMonitorViewFromQML(self):
if not self._monitor_view_qml_path:
return
self._monitor_item = Application.getInstance().createQmlComponent(self._monitor_view_qml_path, {
"OutputDevice": self
})
if self._monitor_item is None:
self._monitor_item = Application.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self})
@pyqtProperty(str, notify=printerTypeChanged)
def printerType(self):
return self._printer_type
## Attempt to establish connection
def connect(self):
self.setConnectionState(ConnectionState.connecting)
self._update_timer.start()
@pyqtProperty(str, notify=printerStateChanged)
def printerState(self):
return self._printer_state
## Attempt to close the connection
def close(self):
self._update_timer.stop()
self.setConnectionState(ConnectionState.closed)
@pyqtProperty(str, notify = jobStateChanged)
def jobState(self):
return self._job_state
def _updatePrinterType(self, printer_type):
if self._printer_type != printer_type:
self._printer_type = printer_type
self.printerTypeChanged.emit()
def _updatePrinterState(self, printer_state):
if self._printer_state != printer_state:
self._printer_state = printer_state
self.printerStateChanged.emit()
def _updateJobState(self, job_state):
if self._job_state != job_state:
self._job_state = job_state
self.jobStateChanged.emit()
@pyqtSlot(str)
def setJobState(self, job_state):
self._setJobState(job_state)
def _setJobState(self, job_state):
Logger.log("w", "_setJobState is not implemented by this output device")
@pyqtSlot()
def startCamera(self):
self._camera_active = True
self._startCamera()
def _startCamera(self):
Logger.log("w", "_startCamera is not implemented by this output device")
@pyqtSlot()
def stopCamera(self):
self._camera_active = False
self._stopCamera()
def _stopCamera(self):
Logger.log("w", "_stopCamera is not implemented by this output device")
@pyqtProperty(str, notify = jobNameChanged)
def jobName(self):
return self._job_name
def setJobName(self, name):
if self._job_name != name:
self._job_name = name
self.jobNameChanged.emit()
## Gives a human-readable address where the device can be found.
@pyqtProperty(str, constant = True)
def address(self):
Logger.log("w", "address is not implemented by this output device.")
## A human-readable name for the device.
@pyqtProperty(str, constant = True)
def name(self):
Logger.log("w", "name is not implemented by this output device.")
return ""
@pyqtProperty(str, notify = errorTextChanged)
def errorText(self):
return self._error_text
## Set the error-text that is shown in the print monitor in case of an error
def setErrorText(self, error_text):
if self._error_text != error_text:
self._error_text = error_text
self.errorTextChanged.emit()
## Ensure that close gets called when object is destroyed
def __del__(self):
self.close()
@pyqtProperty(bool, notify=acceptsCommandsChanged)
def acceptsCommands(self):
return self._accepts_commands
## Set a flag to signal the UI that the printer is not (yet) ready to receive commands
def setAcceptsCommands(self, accepts_commands):
def _setAcceptsCommands(self, accepts_commands):
if self._accepts_commands != accepts_commands:
self._accepts_commands = accepts_commands
self.acceptsCommandsChanged.emit()
## Get the bed temperature of the bed (if any)
# This function is "final" (do not re-implement)
# /sa _getBedTemperature implementation function
@pyqtProperty(float, notify = bedTemperatureChanged)
def bedTemperature(self):
return self._bed_temperature
## Set the (target) bed temperature
# This function is "final" (do not re-implement)
# /param temperature new target temperature of the bed (in deg C)
# /sa _setTargetBedTemperature implementation function
@pyqtSlot(int)
def setTargetBedTemperature(self, temperature):
self._setTargetBedTemperature(temperature)
if self._target_bed_temperature != temperature:
self._target_bed_temperature = temperature
self.targetBedTemperatureChanged.emit()
## The total duration of the time-out to pre-heat the bed, in seconds.
#
# \return The duration of the time-out to pre-heat the bed, in seconds.
@pyqtProperty(int, constant = True)
def preheatBedTimeout(self):
return self._preheat_bed_timeout
## The remaining duration of the pre-heating of the bed.
#
# This is formatted in M:SS format.
# \return The duration of the time-out to pre-heat the bed, formatted.
@pyqtProperty(str, notify = preheatBedRemainingTimeChanged)
def preheatBedRemainingTime(self):
if not self._preheat_bed_timer.isActive():
return ""
period = self._preheat_bed_timer.remainingTime()
if period <= 0:
return ""
minutes, period = divmod(period, 60000) #60000 milliseconds in a minute.
seconds, _ = divmod(period, 1000) #1000 milliseconds in a second.
if minutes <= 0 and seconds <= 0:
return ""
return "%d:%02d" % (minutes, seconds)
## Time the print has been printing.
# Note that timeTotal - timeElapsed should give time remaining.
@pyqtProperty(float, notify = timeElapsedChanged)
def timeElapsed(self):
return self._time_elapsed
## Total time of the print
# Note that timeTotal - timeElapsed should give time remaining.
@pyqtProperty(float, notify=timeTotalChanged)
def timeTotal(self):
return self._time_total
@pyqtSlot(float)
def setTimeTotal(self, new_total):
if self._time_total != new_total:
self._time_total = new_total
self.timeTotalChanged.emit()
@pyqtSlot(float)
def setTimeElapsed(self, time_elapsed):
if self._time_elapsed != time_elapsed:
self._time_elapsed = time_elapsed
self.timeElapsedChanged.emit()
## Home the head of the connected printer
# This function is "final" (do not re-implement)
# /sa _homeHead implementation function
@pyqtSlot()
def homeHead(self):
self._homeHead()
## Home the head of the connected printer
# This is an implementation function and should be overriden by children.
def _homeHead(self):
Logger.log("w", "_homeHead is not implemented by this output device")
## Home the bed of the connected printer
# This function is "final" (do not re-implement)
# /sa _homeBed implementation function
@pyqtSlot()
def homeBed(self):
self._homeBed()
## Home the bed of the connected printer
# This is an implementation function and should be overriden by children.
# /sa homeBed
def _homeBed(self):
Logger.log("w", "_homeBed is not implemented by this output device")
## Protected setter for the bed temperature of the connected printer (if any).
# /parameter temperature Temperature bed needs to go to (in deg celsius)
# /sa setTargetBedTemperature
def _setTargetBedTemperature(self, temperature):
Logger.log("w", "_setTargetBedTemperature is not implemented by this output device")
## Pre-heats the heated bed of the printer.
#
# \param temperature The temperature to heat the bed to, in degrees
# Celsius.
# \param duration How long the bed should stay warm, in seconds.
@pyqtSlot(float, float)
def preheatBed(self, temperature, duration):
Logger.log("w", "preheatBed is not implemented by this output device.")
## Cancels pre-heating the heated bed of the printer.
#
# If the bed is not pre-heated, nothing happens.
@pyqtSlot()
def cancelPreheatBed(self):
Logger.log("w", "cancelPreheatBed is not implemented by this output device.")
## Protected setter for the current bed temperature.
# This simply sets the bed temperature, but ensures that a signal is emitted.
# /param temperature temperature of the bed.
def _setBedTemperature(self, temperature):
if self._bed_temperature != temperature:
self._bed_temperature = temperature
self.bedTemperatureChanged.emit()
## Get the target bed temperature if connected printer (if any)
@pyqtProperty(int, notify = targetBedTemperatureChanged)
def targetBedTemperature(self):
return self._target_bed_temperature
## Set the (target) hotend temperature
# This function is "final" (do not re-implement)
# /param index the index of the hotend that needs to change temperature
# /param temperature The temperature it needs to change to (in deg celsius).
# /sa _setTargetHotendTemperature implementation function
@pyqtSlot(int, int)
def setTargetHotendTemperature(self, index, temperature):
self._setTargetHotendTemperature(index, temperature)
if self._target_hotend_temperatures[index] != temperature:
self._target_hotend_temperatures[index] = temperature
self.targetHotendTemperaturesChanged.emit()
## Implementation function of setTargetHotendTemperature.
# /param index Index of the hotend to set the temperature of
# /param temperature Temperature to set the hotend to (in deg C)
# /sa setTargetHotendTemperature
def _setTargetHotendTemperature(self, index, temperature):
Logger.log("w", "_setTargetHotendTemperature is not implemented by this output device")
@pyqtProperty("QVariantList", notify = targetHotendTemperaturesChanged)
def targetHotendTemperatures(self):
return self._target_hotend_temperatures
@pyqtProperty("QVariantList", notify = hotendTemperaturesChanged)
def hotendTemperatures(self):
return self._hotend_temperatures
## Protected setter for the current hotend temperature.
# This simply sets the hotend temperature, but ensures that a signal is emitted.
# /param index Index of the hotend
# /param temperature temperature of the hotend (in deg C)
def _setHotendTemperature(self, index, temperature):
if self._hotend_temperatures[index] != temperature:
self._hotend_temperatures[index] = temperature
self.hotendTemperaturesChanged.emit()
@pyqtProperty("QVariantList", notify = materialIdChanged)
def materialIds(self):
return self._material_ids
@pyqtProperty("QVariantList", notify = materialIdChanged)
def materialNames(self):
result = []
for material_id in self._material_ids:
if material_id is None:
result.append(i18n_catalog.i18nc("@item:material", "No material loaded"))
continue
containers = self._container_registry.findInstanceContainersMetadata(type = "material", GUID = material_id)
if containers:
result.append(containers[0]["name"])
else:
result.append(i18n_catalog.i18nc("@item:material", "Unknown material"))
return result
## List of the colours of the currently loaded materials.
#
# The list is in order of extruders. If there is no material in an
# extruder, the colour is shown as transparent.
#
# The colours are returned in hex-format AARRGGBB or RRGGBB
# (e.g. #800000ff for transparent blue or #00ff00 for pure green).
@pyqtProperty("QVariantList", notify = materialIdChanged)
def materialColors(self):
result = []
for material_id in self._material_ids:
if material_id is None:
result.append("#00000000") #No material.
continue
containers = self._container_registry.findInstanceContainersMetadata(type = "material", GUID = material_id)
if containers:
result.append(containers[0]["color_code"])
else:
result.append("#00000000") #Unknown material.
return result
## Protected setter for the current material id.
# /param index Index of the extruder
# /param material_id id of the material
def _setMaterialId(self, index, material_id):
if material_id and material_id != "" and material_id != self._material_ids[index]:
Logger.log("d", "Setting material id of hotend %d to %s" % (index, material_id))
self._material_ids[index] = material_id
self.materialIdChanged.emit(index, material_id)
@pyqtProperty("QVariantList", notify = hotendIdChanged)
def hotendIds(self):
return self._hotend_ids
## Protected setter for the current hotend id.
# /param index Index of the extruder
# /param hotend_id id of the hotend
def _setHotendId(self, index, hotend_id):
if hotend_id and hotend_id != self._hotend_ids[index]:
Logger.log("d", "Setting hotend id of hotend %d to %s" % (index, hotend_id))
self._hotend_ids[index] = hotend_id
self.hotendIdChanged.emit(index, hotend_id)
elif not hotend_id:
Logger.log("d", "Removing hotend id of hotend %d.", index)
self._hotend_ids[index] = None
self.hotendIdChanged.emit(index, None)
## Let the user decide if the hotends and/or material should be synced with the printer
# NB: the UX needs to be implemented by the plugin
def materialHotendChangedMessage(self, callback):
Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'")
callback(QMessageBox.Yes)
## Attempt to establish connection
def connect(self):
raise NotImplementedError("connect needs to be implemented")
## Attempt to close the connection
def close(self):
raise NotImplementedError("close needs to be implemented")
@pyqtProperty(bool, notify = connectionStateChanged)
def connectionState(self):
return self._connection_state
## Set the connection state of this output device.
# /param connection_state ConnectionState enum.
def setConnectionState(self, connection_state):
if self._connection_state != connection_state:
self._connection_state = connection_state
self.connectionStateChanged.emit(self._id)
@pyqtProperty(str, notify = connectionTextChanged)
def connectionText(self):
return self._connection_text
## Set a text that is shown on top of the print monitor tab
def setConnectionText(self, connection_text):
if self._connection_text != connection_text:
self._connection_text = connection_text
self.connectionTextChanged.emit()
## Ensure that close gets called when object is destroyed
def __del__(self):
self.close()
## Get the x position of the head.
# This function is "final" (do not re-implement)
@pyqtProperty(float, notify = headPositionChanged)
def headX(self):
return self._head_x
## Get the y position of the head.
# This function is "final" (do not re-implement)
@pyqtProperty(float, notify = headPositionChanged)
def headY(self):
return self._head_y
## Get the z position of the head.
# In some machines it's actually the bed that moves. For convenience sake we simply see it all as head movements.
# This function is "final" (do not re-implement)
@pyqtProperty(float, notify = headPositionChanged)
def headZ(self):
return self._head_z
## Update the saved position of the head
# This function should be called when a new position for the head is received.
def _updateHeadPosition(self, x, y ,z):
position_changed = False
if self._head_x != x:
self._head_x = x
position_changed = True
if self._head_y != y:
self._head_y = y
position_changed = True
if self._head_z != z:
self._head_z = z
position_changed = True
if position_changed:
self.headPositionChanged.emit()
## Set the position of the head.
# In some machines it's actually the bed that moves. For convenience sake we simply see it all as head movements.
# This function is "final" (do not re-implement)
# /param x new x location of the head.
# /param y new y location of the head.
# /param z new z location of the head.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadPosition implementation function
@pyqtSlot("long", "long", "long")
@pyqtSlot("long", "long", "long", "long")
def setHeadPosition(self, x, y, z, speed = 3000):
self._setHeadPosition(x, y , z, speed)
## Set the X position of the head.
# This function is "final" (do not re-implement)
# /param x x position head needs to move to.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadx implementation function
@pyqtSlot("long")
@pyqtSlot("long", "long")
def setHeadX(self, x, speed = 3000):
self._setHeadX(x, speed)
## Set the Y position of the head.
# This function is "final" (do not re-implement)
# /param y y position head needs to move to.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadY implementation function
@pyqtSlot("long")
@pyqtSlot("long", "long")
def setHeadY(self, y, speed = 3000):
self._setHeadY(y, speed)
## Set the Z position of the head.
# In some machines it's actually the bed that moves. For convenience sake we simply see it all as head movements.
# This function is "final" (do not re-implement)
# /param z z position head needs to move to.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadZ implementation function
@pyqtSlot("long")
@pyqtSlot("long", "long")
def setHeadZ(self, z, speed = 3000):
self._setHeadZ(z, speed)
## Move the head of the printer.
# Note that this is a relative move. If you want to move the head to a specific position you can use
# setHeadPosition
# This function is "final" (do not re-implement)
# /param x distance in x to move
# /param y distance in y to move
# /param z distance in z to move
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _moveHead implementation function
@pyqtSlot("long", "long", "long")
@pyqtSlot("long", "long", "long", "long")
def moveHead(self, x = 0, y = 0, z = 0, speed = 3000):
self._moveHead(x, y, z, speed)
## Implementation function of moveHead.
# /param x distance in x to move
# /param y distance in y to move
# /param z distance in z to move
# /param speed Speed by which it needs to move (in mm/minute)
# /sa moveHead
def _moveHead(self, x, y, z, speed):
Logger.log("w", "_moveHead is not implemented by this output device")
## Implementation function of setHeadPosition.
# /param x new x location of the head.
# /param y new y location of the head.
# /param z new z location of the head.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa setHeadPosition
def _setHeadPosition(self, x, y, z, speed):
Logger.log("w", "_setHeadPosition is not implemented by this output device")
## Implementation function of setHeadX.
# /param x new x location of the head.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa setHeadX
def _setHeadX(self, x, speed):
Logger.log("w", "_setHeadX is not implemented by this output device")
## Implementation function of setHeadY.
# /param y new y location of the head.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadY
def _setHeadY(self, y, speed):
Logger.log("w", "_setHeadY is not implemented by this output device")
## Implementation function of setHeadZ.
# /param z new z location of the head.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadZ
def _setHeadZ(self, z, speed):
Logger.log("w", "_setHeadZ is not implemented by this output device")
## Get the progress of any currently active process.
# This function is "final" (do not re-implement)
# /sa _getProgress
# /returns float progress of the process. -1 indicates that there is no process.
@pyqtProperty(float, notify = progressChanged)
def progress(self):
return self._progress
## Set the progress of any currently active process
# /param progress Progress of the process.
def setProgress(self, progress):
if self._progress != progress:
self._progress = progress
self.progressChanged.emit()
## The current processing state of the backend.
class ConnectionState(IntEnum):

View file

@ -13,7 +13,7 @@ class BuildPlateDecorator(SceneNodeDecorator):
# Make sure that groups are set correctly
# setBuildPlateForSelection in CuraActions makes sure that no single childs are set.
self._build_plate_number = nr
if issubclass(type(self._node), CuraSceneNode):
if isinstance(self._node, CuraSceneNode):
self._node.transformChanged() # trigger refresh node without introducing a new signal
if self._node and self._node.callDecoration("isGroup"):
for child in self._node.getChildren():

View file

@ -24,7 +24,10 @@ class ConvexHullNode(SceneNode):
self._original_parent = parent
# Color of the drawn convex hull
if Application.getInstance().hasGui():
self._color = Color(*Application.getInstance().getTheme().getColor("convex_hull").getRgb())
else:
self._color = Color(0, 0, 0)
# The y-coordinate of the convex hull mesh. Must not be 0, to prevent z-fighting.
self._mesh_height = 0.1
@ -65,7 +68,7 @@ class ConvexHullNode(SceneNode):
ConvexHullNode.shader.setUniformValue("u_opacity", 0.6)
if self.getParent():
if self.getMeshData() and issubclass(type(self._node), SceneNode) and self._node.callDecoration("getBuildPlateNumber") == Application.getInstance().getBuildPlateModel().activeBuildPlate:
if self.getMeshData() and isinstance(self._node, SceneNode) and self._node.callDecoration("getBuildPlateNumber") == Application.getInstance().getBuildPlateModel().activeBuildPlate:
renderer.queueNode(self, transparent = True, shader = ConvexHullNode.shader, backface_cull = True, sort = -8)
if self._convex_hull_head_mesh:
renderer.queueNode(self, shader = ConvexHullNode.shader, transparent = True, mesh = self._convex_hull_head_mesh, backface_cull = True, sort = -8)

View file

@ -10,9 +10,12 @@ from UM.Application import Application
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection
from UM.Signal import Signal
class CuraSceneController(QObject):
activeBuildPlateChanged = Signal()
def __init__(self, objects_model: ObjectsModel, build_plate_model: BuildPlateModel):
super().__init__()
@ -30,7 +33,7 @@ class CuraSceneController(QObject):
source = args[0]
else:
source = None
if not issubclass(type(source), SceneNode):
if not isinstance(source, SceneNode):
return
max_build_plate = self._calcMaxBuildPlate()
changed = False
@ -41,6 +44,14 @@ class CuraSceneController(QObject):
self._build_plate_model.setMaxBuildPlate(self._max_build_plate)
build_plates = [{"name": "Build Plate %d" % (i + 1), "buildPlateNumber": i} for i in range(self._max_build_plate + 1)]
self._build_plate_model.setItems(build_plates)
if self._active_build_plate > self._max_build_plate:
build_plate_number = 0
if self._last_selected_index >= 0: # go to the buildplate of the item you last selected
item = self._objects_model.getItem(self._last_selected_index)
if "node" in item:
node = item["node"]
build_plate_number = node.callDecoration("getBuildPlateNumber")
self.setActiveBuildPlate(build_plate_number)
# self.buildPlateItemsChanged.emit() # TODO: necessary after setItems?
def _calcMaxBuildPlate(self):
@ -48,6 +59,8 @@ class CuraSceneController(QObject):
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
if node.callDecoration("isSliceable"):
build_plate_number = node.callDecoration("getBuildPlateNumber")
if build_plate_number is None:
build_plate_number = 0
max_build_plate = max(build_plate_number, max_build_plate)
return max_build_plate
@ -75,11 +88,11 @@ class CuraSceneController(QObject):
# Single select
item = self._objects_model.getItem(index)
node = item["node"]
Selection.clear()
Selection.add(node)
build_plate_number = node.callDecoration("getBuildPlateNumber")
if build_plate_number is not None and build_plate_number != -1:
self._build_plate_model.setActiveBuildPlate(build_plate_number)
self.setActiveBuildPlate(build_plate_number)
Selection.clear()
Selection.add(node)
self._last_selected_index = index
@ -93,6 +106,7 @@ class CuraSceneController(QObject):
self._build_plate_model.setActiveBuildPlate(nr)
self._objects_model.setActiveBuildPlate(nr)
self.activeBuildPlateChanged.emit()
@staticmethod
def createCuraSceneController():

View file

@ -1,7 +1,9 @@
from typing import List
from UM.Application import Application
from UM.Logger import Logger
from UM.Scene.SceneNode import SceneNode
from copy import deepcopy
from cura.Settings.ExtrudersModel import ExtrudersModel
## Scene nodes that are models are only seen when selecting the corresponding build plate
@ -9,7 +11,7 @@ from copy import deepcopy
class CuraSceneNode(SceneNode):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._outside_buildarea = True
self._outside_buildarea = False
def setOutsideBuildArea(self, new_value):
self._outside_buildarea = new_value
@ -23,6 +25,53 @@ class CuraSceneNode(SceneNode):
def isSelectable(self) -> bool:
return super().isSelectable() and self.callDecoration("getBuildPlateNumber") == Application.getInstance().getBuildPlateModel().activeBuildPlate
## Get the extruder used to print this node. If there is no active node, then the extruder in position zero is returned
# TODO The best way to do it is by adding the setActiveExtruder decorator to every node when is loaded
def getPrintingExtruder(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
per_mesh_stack = self.callDecoration("getStack")
extruders = list(global_container_stack.extruders.values())
# Use the support extruder instead of the active extruder if this is a support_mesh
if per_mesh_stack:
if per_mesh_stack.getProperty("support_mesh", "value"):
return extruders[int(global_container_stack.getProperty("support_extruder_nr", "value"))]
# It's only set if you explicitly choose an extruder
extruder_id = self.callDecoration("getActiveExtruder")
for extruder in extruders:
# Find out the extruder if we know the id.
if extruder_id is not None:
if extruder_id == extruder.getId():
return extruder
else: # If the id is unknown, then return the extruder in the position 0
try:
if extruder.getMetaDataEntry("position", default = "0") == "0": # Check if the position is zero
return extruder
except ValueError:
continue
# This point should never be reached
return None
## Return the color of the material used to print this model
def getDiffuseColor(self) -> List[float]:
printing_extruder = self.getPrintingExtruder()
material_color = "#808080" # Fallback color
if printing_extruder is not None and printing_extruder.material:
material_color = printing_extruder.material.getMetaDataEntry("color_code", default = material_color)
# Colors are passed as rgb hex strings (eg "#ffffff"), and the shader needs
# an rgba list of floats (eg [1.0, 1.0, 1.0, 1.0])
return [
int(material_color[1:3], 16) / 255,
int(material_color[3:5], 16) / 255,
int(material_color[5:7], 16) / 255,
1.0
]
## Taken from SceneNode, but replaced SceneNode with CuraSceneNode
def __deepcopy__(self, memo):
copy = CuraSceneNode()

View file

@ -3,7 +3,7 @@
import copy
import os.path
import urllib
import urllib.parse
import uuid
from typing import Any, Dict, List, Union
@ -459,7 +459,7 @@ class ContainerManager(QObject):
# \return \type{Dict} dict with a 'status' key containing the string 'success' or 'error', and a 'message' key
# containing a message for the user
@pyqtSlot(QUrl, result = "QVariantMap")
def importContainer(self, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]:
def importMaterialContainer(self, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]:
if not file_url_or_string:
return { "status": "error", "message": "Invalid path"}
@ -486,12 +486,14 @@ class ContainerManager(QObject):
container = container_type(container_id)
try:
with open(file_url, "rt") as f:
with open(file_url, "rt", encoding = "utf-8") as f:
container.deserialize(f.read())
except PermissionError:
return { "status": "error", "message": "Permission denied when trying to read the file"}
except Exception as ex:
return {"status": "error", "message": str(ex)}
container.setName(container_id)
container.setDirty(True)
self._container_registry.addContainer(container)
@ -816,6 +818,22 @@ class ContainerManager(QObject):
ContainerRegistry.getInstance().addContainer(container_to_add)
return self._getMaterialContainerIdForActiveMachine(clone_of_original)
## Create a duplicate of a material or it's original entry
#
# \return \type{str} the id of the newly created container.
@pyqtSlot(str, result = str)
def duplicateOriginalMaterial(self, material_id):
# check if the given material has a base file (i.e. was shipped by default)
base_file = self.getContainerMetaDataEntry(material_id, "base_file")
if base_file == "":
# there is no base file, so duplicate by ID
return self.duplicateMaterial(material_id)
else:
# there is a base file, so duplicate the original material
return self.duplicateMaterial(base_file)
## Create a new material by cloning Generic PLA for the current material diameter and setting the GUID to something unqiue
#
# \return \type{str} the id of the newly created container.

View file

@ -94,7 +94,7 @@ class CuraContainerRegistry(ContainerRegistry):
def _containerExists(self, container_type, container_name):
container_class = ContainerStack if container_type == "machine" else InstanceContainer
return self.findContainersMetadata(id = container_name, type = container_type, ignore_case = True) or \
return self.findContainersMetadata(container_type = container_class, id = container_name, type = container_type, ignore_case = True) or \
self.findContainersMetadata(container_type = container_class, name = container_name, type = container_type)
## Exports an profile to a file
@ -202,16 +202,43 @@ class CuraContainerRegistry(ContainerRegistry):
for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
if meta_data["profile_reader"][0]["extension"] != extension:
continue
profile_reader = plugin_registry.getPluginObject(plugin_id)
try:
profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
except Exception as e:
# Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None.
Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name,profile_reader.getPluginId(), str(e))
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, str(e))}
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, "\n" + str(e))}
if profile_or_list:
# Ensure it is always a list of profiles
if not isinstance(profile_or_list, list):
profile_or_list = [profile_or_list]
# First check if this profile is suitable for this machine
global_profile = None
if len(profile_or_list) == 1:
global_profile = profile_or_list[0]
else:
for profile in profile_or_list:
if not profile.getMetaDataEntry("extruder"):
global_profile = profile
break
if not global_profile:
Logger.log("e", "Incorrect profile [%s]. Could not find global profile", file_name)
return { "status": "error",
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name)}
profile_definition = global_profile.getMetaDataEntry("definition")
expected_machine_definition = "fdmprinter"
if parseBool(global_container_stack.getMetaDataEntry("has_machine_quality", "False")):
expected_machine_definition = global_container_stack.getMetaDataEntry("quality_definition")
if not expected_machine_definition:
expected_machine_definition = global_container_stack.definition.getId()
if expected_machine_definition is not None and profile_definition is not None and profile_definition != expected_machine_definition:
Logger.log("e", "Profile [%s] is for machine [%s] but the current active machine is [%s]. Will not import the profile", file_name)
return { "status": "error",
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "The machine defined in profile <filename>{0}</filename> doesn't match with your current machine, could not import it.", file_name)}
name_seed = os.path.splitext(os.path.basename(file_name))[0]
new_name = self.uniqueName(name_seed)
@ -219,6 +246,41 @@ class CuraContainerRegistry(ContainerRegistry):
if type(profile_or_list) is not list:
profile_or_list = [profile_or_list]
# Make sure that there are also extruder stacks' quality_changes, not just one for the global stack
if len(profile_or_list) == 1:
global_profile = profile_or_list[0]
extruder_profiles = []
for idx, extruder in enumerate(global_container_stack.extruders.values()):
profile_id = ContainerRegistry.getInstance().uniqueName(global_container_stack.getId() + "_extruder_" + str(idx + 1))
profile = InstanceContainer(profile_id)
profile.setName(global_profile.getName())
profile.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
profile.addMetaDataEntry("type", "quality_changes")
profile.addMetaDataEntry("definition", global_profile.getMetaDataEntry("definition"))
profile.addMetaDataEntry("quality_type", global_profile.getMetaDataEntry("quality_type"))
profile.addMetaDataEntry("extruder", extruder.getId())
profile.setDirty(True)
if idx == 0:
# move all per-extruder settings to the first extruder's quality_changes
for qc_setting_key in global_profile.getAllKeys():
settable_per_extruder = global_container_stack.getProperty(qc_setting_key,
"settable_per_extruder")
if settable_per_extruder:
setting_value = global_profile.getProperty(qc_setting_key, "value")
setting_definition = global_container_stack.getSettingDefinition(qc_setting_key)
new_instance = SettingInstance(setting_definition, profile)
new_instance.setProperty("value", setting_value)
new_instance.resetState() # Ensure that the state is not seen as a user state.
profile.addInstance(new_instance)
profile.setDirty(True)
global_profile.removeInstance(qc_setting_key, postpone_emit=True)
extruder_profiles.append(profile)
for profile in extruder_profiles:
profile_or_list.append(profile)
# Import all profiles
for profile_index, profile in enumerate(profile_or_list):
if profile_index == 0:
@ -246,6 +308,9 @@ class CuraContainerRegistry(ContainerRegistry):
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}
# This message is throw when the profile reader doesn't find any profile in the file
return {"status": "error", "message": catalog.i18nc("@info:status", "File {0} does not contain any valid profile.", file_name)}
# If it hasn't returned by now, none of the plugins loaded the profile successfully.
return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)}
@ -266,9 +331,13 @@ class CuraContainerRegistry(ContainerRegistry):
profile.setDirty(True) # Ensure the profiles are correctly saved
new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile"))
profile._id = new_id
profile.setMetaDataEntry("id", new_id)
profile.setName(new_name)
# Set the unique Id to the profile, so it's generating a new one even if the user imports the same profile
# It also solves an issue with importing profiles from G-Codes
profile.setMetaDataEntry("id", new_id)
if "type" in profile.getMetaData():
profile.setMetaDataEntry("type", "quality_changes")
else:
@ -415,7 +484,14 @@ class CuraContainerRegistry(ContainerRegistry):
if not extruder_stacks:
self.addExtruderStackForSingleExtrusionMachine(container, "fdmextruder")
def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id):
#
# new_global_quality_changes is optional. It is only used in project loading for a scenario like this:
# - override the current machine
# - create new for custom quality profile
# new_global_quality_changes is the new global quality changes container in this scenario.
# create_new_ids indicates if new unique ids must be created
#
def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id, new_global_quality_changes = None, create_new_ids = True):
new_extruder_id = extruder_id
extruder_definitions = self.findDefinitionContainers(id = new_extruder_id)
@ -424,7 +500,7 @@ class CuraContainerRegistry(ContainerRegistry):
return
extruder_definition = extruder_definitions[0]
unique_name = self.uniqueName(machine.getName() + " " + new_extruder_id)
unique_name = self.uniqueName(machine.getName() + " " + new_extruder_id) if create_new_ids else machine.getName() + " " + new_extruder_id
extruder_stack = ExtruderStack.ExtruderStack(unique_name)
extruder_stack.setName(extruder_definition.getName())
@ -434,7 +510,7 @@ class CuraContainerRegistry(ContainerRegistry):
from cura.CuraApplication import CuraApplication
# create a new definition_changes container for the extruder stack
definition_changes_id = self.uniqueName(extruder_stack.getId() + "_settings")
definition_changes_id = self.uniqueName(extruder_stack.getId() + "_settings") if create_new_ids else extruder_stack.getId() + "_settings"
definition_changes_name = definition_changes_id
definition_changes = InstanceContainer(definition_changes_id)
definition_changes.setName(definition_changes_name)
@ -461,14 +537,15 @@ class CuraContainerRegistry(ContainerRegistry):
extruder_stack.setDefinitionChanges(definition_changes)
# create empty user changes container otherwise
user_container_id = self.uniqueName(extruder_stack.getId() + "_user")
user_container_id = self.uniqueName(extruder_stack.getId() + "_user") if create_new_ids else extruder_stack.getId() + "_user"
user_container_name = user_container_id
user_container = InstanceContainer(user_container_id)
user_container.setName(user_container_name)
user_container.addMetaDataEntry("type", "user")
user_container.addMetaDataEntry("machine", extruder_stack.getId())
user_container.addMetaDataEntry("machine", machine.getId())
user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
user_container.setDefinition(machine.definition.getId())
user_container.setMetaDataEntry("extruder", extruder_stack.getId())
if machine.userChanges:
# for the newly created extruder stack, we need to move all "per-extruder" settings to the user changes
@ -511,29 +588,115 @@ class CuraContainerRegistry(ContainerRegistry):
quality_id = "empty_quality"
extruder_stack.setQualityById(quality_id)
if machine.qualityChanges.getId() not in ("empty", "empty_quality_changes"):
extruder_quality_changes_container = self.findInstanceContainers(name = machine.qualityChanges.getName(), extruder = extruder_id)
machine_quality_changes = machine.qualityChanges
if new_global_quality_changes is not None:
machine_quality_changes = new_global_quality_changes
if machine_quality_changes.getId() not in ("empty", "empty_quality_changes"):
extruder_quality_changes_container = self.findInstanceContainers(name = machine_quality_changes.getName(), extruder = extruder_id)
if extruder_quality_changes_container:
extruder_quality_changes_container = extruder_quality_changes_container[0]
quality_changes_id = extruder_quality_changes_container.getId()
extruder_stack.setQualityChangesById(quality_changes_id)
else:
# Some extruder quality_changes containers can be created at runtime as files in the qualities
# folder. Those files won't be loaded in the registry immediately. So we also need to search
# the folder to see if the quality_changes exists.
extruder_quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine.qualityChanges.getName())
extruder_quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName())
if extruder_quality_changes_container:
quality_changes_id = extruder_quality_changes_container.getId()
extruder_quality_changes_container.addMetaDataEntry("extruder", extruder_stack.definition.getId())
extruder_stack.setQualityChangesById(quality_changes_id)
else:
# if we still cannot find a quality changes container for the extruder, create a new one
container_name = machine_quality_changes.getName()
container_id = self.uniqueName(extruder_stack.getId() + "_qc_" + container_name)
extruder_quality_changes_container = InstanceContainer(container_id)
extruder_quality_changes_container.setName(container_name)
extruder_quality_changes_container.addMetaDataEntry("type", "quality_changes")
extruder_quality_changes_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
extruder_quality_changes_container.addMetaDataEntry("extruder", extruder_stack.definition.getId())
extruder_quality_changes_container.addMetaDataEntry("quality_type", machine_quality_changes.getMetaDataEntry("quality_type"))
extruder_quality_changes_container.setDefinition(machine_quality_changes.getDefinition().getId())
self.addContainer(extruder_quality_changes_container)
extruder_stack.qualityChanges = extruder_quality_changes_container
if not extruder_quality_changes_container:
Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]",
machine.qualityChanges.getName(), extruder_stack.getId())
machine_quality_changes.getName(), extruder_stack.getId())
else:
# move all per-extruder settings to the extruder's quality changes
for qc_setting_key in machine_quality_changes.getAllKeys():
settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder")
if settable_per_extruder:
setting_value = machine_quality_changes.getProperty(qc_setting_key, "value")
setting_definition = machine.getSettingDefinition(qc_setting_key)
new_instance = SettingInstance(setting_definition, definition_changes)
new_instance.setProperty("value", setting_value)
new_instance.resetState() # Ensure that the state is not seen as a user state.
extruder_quality_changes_container.addInstance(new_instance)
extruder_quality_changes_container.setDirty(True)
machine_quality_changes.removeInstance(qc_setting_key, postpone_emit=True)
else:
extruder_stack.setQualityChangesById("empty_quality_changes")
self.addContainer(extruder_stack)
# Also need to fix the other qualities that are suitable for this machine. Those quality changes may still have
# per-extruder settings in the container for the machine instead of the extruder.
if machine_quality_changes.getId() not in ("empty", "empty_quality_changes"):
quality_changes_machine_definition_id = machine_quality_changes.getDefinition().getId()
else:
whole_machine_definition = machine.definition
machine_entry = machine.definition.getMetaDataEntry("machine")
if machine_entry is not None:
container_registry = ContainerRegistry.getInstance()
whole_machine_definition = container_registry.findDefinitionContainers(id = machine_entry)[0]
quality_changes_machine_definition_id = "fdmprinter"
if whole_machine_definition.getMetaDataEntry("has_machine_quality"):
quality_changes_machine_definition_id = machine.definition.getMetaDataEntry("quality_definition",
whole_machine_definition.getId())
qcs = self.findInstanceContainers(type = "quality_changes", definition = quality_changes_machine_definition_id)
qc_groups = {} # map of qc names -> qc containers
for qc in qcs:
qc_name = qc.getName()
if qc_name not in qc_groups:
qc_groups[qc_name] = []
qc_groups[qc_name].append(qc)
# try to find from the quality changes cura directory too
quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName())
if quality_changes_container:
qc_groups[qc_name].append(quality_changes_container)
for qc_name, qc_list in qc_groups.items():
qc_dict = {"global": None, "extruders": []}
for qc in qc_list:
extruder_def_id = qc.getMetaDataEntry("extruder")
if extruder_def_id is not None:
qc_dict["extruders"].append(qc)
else:
qc_dict["global"] = qc
if qc_dict["global"] is not None and len(qc_dict["extruders"]) == 1:
# move per-extruder settings
for qc_setting_key in qc_dict["global"].getAllKeys():
settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder")
if settable_per_extruder:
setting_value = qc_dict["global"].getProperty(qc_setting_key, "value")
setting_definition = machine.getSettingDefinition(qc_setting_key)
new_instance = SettingInstance(setting_definition, definition_changes)
new_instance.setProperty("value", setting_value)
new_instance.resetState() # Ensure that the state is not seen as a user state.
qc_dict["extruders"][0].addInstance(new_instance)
qc_dict["extruders"][0].setDirty(True)
qc_dict["global"].removeInstance(qc_setting_key, postpone_emit=True)
# Set next stack at the end
extruder_stack.setNextStack(machine)
@ -549,7 +712,7 @@ class CuraContainerRegistry(ContainerRegistry):
if not os.path.isfile(file_path):
continue
parser = configparser.ConfigParser()
parser = configparser.ConfigParser(interpolation=None)
try:
parser.read([file_path])
except:
@ -562,9 +725,12 @@ class CuraContainerRegistry(ContainerRegistry):
if parser["general"]["name"] == name:
# load the container
container_id = os.path.basename(file_path).replace(".inst.cfg", "")
if self.findInstanceContainers(id = container_id):
# this container is already in the registry, skip it
continue
instance_container = InstanceContainer(container_id)
with open(file_path, "r") as f:
with open(file_path, "r", encoding = "utf-8") as f:
serialized = f.read()
instance_container.deserialize(serialized, file_path)
self.addContainer(instance_container)

View file

@ -144,7 +144,7 @@ class CuraContainerStack(ContainerStack):
## Set the material container.
#
# \param new_quality_changes The new material container. It is expected to have a "type" metadata entry with the value "quality_changes".
# \param new_material The new material container. It is expected to have a "type" metadata entry with the value "material".
def setMaterial(self, new_material: InstanceContainer, postpone_emit = False) -> None:
self.replaceContainer(_ContainerIndexes.Material, new_material, postpone_emit = postpone_emit)
@ -155,7 +155,7 @@ class CuraContainerStack(ContainerStack):
# to whatever the machine definition specifies as "preferred" container, or a fallback value. See findDefaultMaterial
# for details.
#
# \param new_quality_changes_id The ID of the new material container.
# \param new_material_id The ID of the new material container.
#
# \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
def setMaterialById(self, new_material_id: str) -> None:
@ -182,7 +182,7 @@ class CuraContainerStack(ContainerStack):
## Set the variant container.
#
# \param new_quality_changes The new variant container. It is expected to have a "type" metadata entry with the value "quality_changes".
# \param new_variant The new variant container. It is expected to have a "type" metadata entry with the value "variant".
def setVariant(self, new_variant: InstanceContainer) -> None:
self.replaceContainer(_ContainerIndexes.Variant, new_variant)
@ -193,13 +193,13 @@ class CuraContainerStack(ContainerStack):
# to whatever the machine definition specifies as "preferred" container, or a fallback value. See findDefaultVariant
# for details.
#
# \param new_quality_changes_id The ID of the new variant container.
# \param new_variant_id The ID of the new variant container.
#
# \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
def setVariantById(self, new_variant_id: str) -> None:
variant = self._empty_variant
if new_variant_id == "default":
new_variant = self.findDefaultVariant()
new_variant = self.findDefaultVariantBuildplate() if self.getMetaDataEntry("type") == "machine" else self.findDefaultVariant()
if new_variant:
variant = new_variant
else:
@ -220,13 +220,13 @@ class CuraContainerStack(ContainerStack):
## Set the definition changes container.
#
# \param new_quality_changes The new definition changes container. It is expected to have a "type" metadata entry with the value "quality_changes".
# \param new_definition_changes The new definition changes container. It is expected to have a "type" metadata entry with the value "definition_changes".
def setDefinitionChanges(self, new_definition_changes: InstanceContainer) -> None:
self.replaceContainer(_ContainerIndexes.DefinitionChanges, new_definition_changes)
## Set the definition changes container by an ID.
#
# \param new_quality_changes_id The ID of the new definition changes container.
# \param new_definition_changes_id The ID of the new definition changes container.
#
# \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
def setDefinitionChangesById(self, new_definition_changes_id: str) -> None:
@ -245,13 +245,13 @@ class CuraContainerStack(ContainerStack):
## Set the definition container.
#
# \param new_quality_changes The new definition container. It is expected to have a "type" metadata entry with the value "quality_changes".
# \param new_definition The new definition container. It is expected to have a "type" metadata entry with the value "definition".
def setDefinition(self, new_definition: DefinitionContainerInterface) -> None:
self.replaceContainer(_ContainerIndexes.Definition, new_definition)
## Set the definition container by an ID.
#
# \param new_quality_changes_id The ID of the new definition container.
# \param new_definition_id The ID of the new definition container.
#
# \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
def setDefinitionById(self, new_definition_id: str) -> None:
@ -435,6 +435,51 @@ class CuraContainerStack(ContainerStack):
Logger.log("w", "Could not find a valid default variant for stack {stack}", stack = self.id)
return None
## Find the global variant that should be used as "default". This is used for the buildplates.
#
# This will search for variants that match the current definition and pick the preferred one,
# if specified by the machine definition.
#
# The following criteria are used to find the default global variant:
# - If the machine definition does not have a metadata entry "has_variant_buildplates" set to True, return None
# - The definition of the variant should be the same as the machine definition for this stack.
# - The container should have a metadata entry "type" with value "variant" and "hardware_type" with value "buildplate".
# - If the machine definition has a metadata entry "preferred_variant_buildplate", filter the variant IDs based on that.
#
# \return The container that should be used as default, or None if nothing was found or the machine does not use variants.
#
# \note This method assumes the stack has a valid machine definition.
def findDefaultVariantBuildplate(self) -> Optional[ContainerInterface]:
definition = self._getMachineDefinition()
# has_variant_buildplates can be overridden in other containers and stacks.
# In the case of UM2, it is overridden in the GlobalStack
if not self.getMetaDataEntry("has_variant_buildplates"):
# If the machine does not use variants, we should never set a variant.
return None
# First add any variant. Later, overwrite with preference if the preference is valid.
variant = None
definition_id = self._findInstanceContainerDefinitionId(definition)
variants = ContainerRegistry.getInstance().findInstanceContainers(definition = definition_id, type = "variant", hardware_type = "buildplate")
if variants:
variant = variants[0]
preferred_variant_buildplate_id = definition.getMetaDataEntry("preferred_variant_buildplate")
if preferred_variant_buildplate_id:
preferred_variant_buildplates = ContainerRegistry.getInstance().findInstanceContainers(id = preferred_variant_buildplate_id, definition = definition_id, type = "variant")
if preferred_variant_buildplates:
variant = preferred_variant_buildplates[0]
else:
Logger.log("w", "The preferred variant buildplate \"{variant}\" of stack {stack} does not exist or is not a variant.",
variant = preferred_variant_buildplate_id, stack = self.id)
# And leave it at the default variant.
if variant:
return variant
Logger.log("w", "Could not find a valid default buildplate variant for stack {stack}", stack = self.id)
return None
## Find the material that should be used as "default" material.
#
# This will search for materials that match the current definition and pick the preferred one,

View file

@ -63,6 +63,7 @@ class CuraStackBuilder:
next_stack = new_global_stack
)
new_global_stack.addExtruder(new_extruder)
registry.addContainer(new_extruder)
else:
# create extruder stack for each found extruder definition
for extruder_definition in registry.findDefinitionContainers(machine = machine_definition.id):
@ -81,6 +82,11 @@ class CuraStackBuilder:
next_stack = new_global_stack
)
new_global_stack.addExtruder(new_extruder)
registry.addContainer(new_extruder)
# Register the global stack after the extruder stacks are created. This prevents the registry from adding another
# extruder stack because the global stack didn't have one yet (which is enforced since Cura 3.1).
registry.addContainer(new_global_stack)
return new_global_stack
@ -135,9 +141,7 @@ class CuraStackBuilder:
# Only add the created containers to the registry after we have set all the other
# properties. This makes the create operation more transactional, since any problems
# setting properties will not result in incomplete containers being added.
registry = ContainerRegistry.getInstance()
registry.addContainer(stack)
registry.addContainer(user_container)
ContainerRegistry.getInstance().addContainer(user_container)
return stack
@ -181,9 +185,7 @@ class CuraStackBuilder:
if "quality_changes" in kwargs:
stack.setQualityChangesById(kwargs["quality_changes"])
registry = ContainerRegistry.getInstance()
registry.addContainer(stack)
registry.addContainer(user_container)
ContainerRegistry.getInstance().addContainer(user_container)
return stack

View file

@ -49,6 +49,9 @@ class ExtruderManager(QObject):
## Notify when the user switches the currently active extruder.
activeExtruderChanged = pyqtSignal()
## The signal notifies subscribers if extruders are added
extrudersAdded = pyqtSignal()
## Gets the unique identifier of the currently active extruder stack.
#
# The currently active extruder stack is the stack that is currently being
@ -270,7 +273,7 @@ class ExtruderManager(QObject):
return []
# Get the extruders of all printable meshes in the scene
meshes = [node for node in DepthFirstIterator(scene_root) if type(node) is SceneNode and node.isSelectable()]
meshes = [node for node in DepthFirstIterator(scene_root) if isinstance(node, SceneNode) and node.isSelectable()]
for mesh in meshes:
extruder_stack_id = mesh.callDecoration("getActiveExtruder")
if not extruder_stack_id:
@ -404,8 +407,15 @@ class ExtruderManager(QObject):
extruder_train.setNextStack(global_stack)
extruders_changed = True
# FIX: We have to remove those settings here because we know that those values have been copied to all
# the extruders at this point.
for key in ("material_diameter", "machine_nozzle_size"):
if global_stack.definitionChanges.hasProperty(key, "value"):
global_stack.definitionChanges.removeInstance(key, postpone_emit = True)
if extruders_changed:
self.extrudersChanged.emit(global_stack_id)
self.extrudersAdded.emit()
self.setActiveExtruderIndex(0)
## Get all extruder values for a certain setting.
@ -492,6 +502,95 @@ class ExtruderManager(QObject):
def getInstanceExtruderValues(self, key):
return ExtruderManager.getExtruderValues(key)
## Updates the material container to a material that matches the material diameter set for the printer
def updateMaterialForDiameter(self, extruder_position: int):
global_stack = Application.getInstance().getGlobalContainerStack()
if not global_stack:
return
if not global_stack.getMetaDataEntry("has_materials", False):
return
extruder_stack = global_stack.extruders[str(extruder_position)]
material_diameter = extruder_stack.material.getProperty("material_diameter", "value")
if not material_diameter:
# in case of "empty" material
material_diameter = 0
material_approximate_diameter = str(round(material_diameter))
material_diameter = extruder_stack.definitionChanges.getProperty("material_diameter", "value")
setting_provider = extruder_stack
if not material_diameter:
if extruder_stack.definition.hasProperty("material_diameter", "value"):
material_diameter = extruder_stack.definition.getProperty("material_diameter", "value")
else:
material_diameter = global_stack.definition.getProperty("material_diameter", "value")
setting_provider = global_stack
if isinstance(material_diameter, SettingFunction):
material_diameter = material_diameter(setting_provider)
machine_approximate_diameter = str(round(material_diameter))
if material_approximate_diameter != machine_approximate_diameter:
Logger.log("i", "The the currently active material(s) do not match the diameter set for the printer. Finding alternatives.")
if global_stack.getMetaDataEntry("has_machine_materials", False):
materials_definition = global_stack.definition.getId()
has_material_variants = global_stack.getMetaDataEntry("has_variants", False)
else:
materials_definition = "fdmprinter"
has_material_variants = False
old_material = extruder_stack.material
search_criteria = {
"type": "material",
"approximate_diameter": machine_approximate_diameter,
"material": old_material.getMetaDataEntry("material", "value"),
"brand": old_material.getMetaDataEntry("brand", "value"),
"supplier": old_material.getMetaDataEntry("supplier", "value"),
"color_name": old_material.getMetaDataEntry("color_name", "value"),
"definition": materials_definition
}
if has_material_variants:
search_criteria["variant"] = extruder_stack.variant.getId()
container_registry = Application.getInstance().getContainerRegistry()
empty_material = container_registry.findInstanceContainers(id = "empty_material")[0]
if old_material == empty_material:
search_criteria.pop("material", None)
search_criteria.pop("supplier", None)
search_criteria.pop("brand", None)
search_criteria.pop("definition", None)
search_criteria["id"] = extruder_stack.getMetaDataEntry("preferred_material")
materials = container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Same material with new diameter is not found, search for generic version of the same material type
search_criteria.pop("supplier", None)
search_criteria.pop("brand", None)
search_criteria["color_name"] = "Generic"
materials = container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Generic material with new diameter is not found, search for preferred material
search_criteria.pop("color_name", None)
search_criteria.pop("material", None)
search_criteria["id"] = extruder_stack.getMetaDataEntry("preferred_material")
materials = container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Preferred material with new diameter is not found, search for any material
search_criteria.pop("id", None)
materials = container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Just use empty material as a final fallback
materials = [empty_material]
Logger.log("i", "Selecting new material: %s", materials[0].getId())
extruder_stack.material = materials[0]
## Get the value for a setting from a specific extruder.
#
# This is exposed to SettingFunction to use in value functions.

View file

@ -3,6 +3,7 @@
from typing import Any, TYPE_CHECKING, Optional
from UM.Application import Application
from UM.Decorators import override
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
from UM.Settings.ContainerStack import ContainerStack
@ -18,10 +19,6 @@ if TYPE_CHECKING:
from cura.Settings.GlobalStack import GlobalStack
_EXTRUDER_SPECIFIC_DEFINITION_CHANGES_SETTINGS = ["machine_nozzle_size",
"material_diameter"]
## Represents an Extruder and its related containers.
#
#
@ -53,10 +50,43 @@ class ExtruderStack(CuraContainerStack):
# when we are upgrading a definition_changes container file, there is NO guarantee that other files such as
# machine an extruder stack files are upgraded before this, so we cannot read those files assuming they are in
# the latest format.
if self.getMetaDataEntry("position") == "0":
for key in _EXTRUDER_SPECIFIC_DEFINITION_CHANGES_SETTINGS:
setting_value = stack.definitionChanges.getProperty(key, "value")
if setting_value is None:
#
# MORE:
# For single-extrusion machines, nozzle size is saved in the global stack, so the nozzle size value should be
# carried to the first extruder.
# For material diameter, it was supposed to be applied to all extruders, so its value should be copied to all
# extruders.
keys_to_copy = ["material_diameter", "machine_nozzle_size"] # these will be copied over to all extruders
for key in keys_to_copy:
# Only copy the value when this extruder doesn't have the value.
if self.definitionChanges.hasProperty(key, "value"):
continue
# WARNING: this might be very dangerous and should be refactored ASAP!
#
# We cannot add a setting definition of "material_diameter" into the extruder's definition at runtime
# because all other machines which uses "fdmextruder" as the extruder definition will be affected.
#
# The problem is that single extrusion machines have their default material diameter defined in the global
# definitions. Now we automatically create an extruder stack for those machines using "fdmextruder"
# definition, which doesn't have the specific "material_diameter" and "machine_nozzle_size" defined for
# each machine. This results in wrong values which can be found in the MachineSettings dialog.
#
# To solve this, we put "material_diameter" back into the "fdmextruder" definition because modifying it in
# the extruder definition will affect all machines which uses the "fdmextruder" definition. Moreover, now
# we also check the value defined in the machine definition. If present, the value defined in the global
# stack's definition changes container will be copied. Otherwise, we will check if the default values in the
# machine definition and the extruder definition are the same, and if not, the default value in the machine
# definition will be copied to the extruder stack's definition changes.
#
setting_value_in_global_def_changes = stack.definitionChanges.getProperty(key, "value")
setting_value_in_global_def = stack.definition.getProperty(key, "value")
setting_value = setting_value_in_global_def
if setting_value_in_global_def_changes is not None:
setting_value = setting_value_in_global_def_changes
if setting_value == self.definition.getProperty(key, "value"):
continue
setting_definition = stack.getSettingDefinition(key)
@ -66,7 +96,20 @@ class ExtruderStack(CuraContainerStack):
self.definitionChanges.addInstance(new_instance)
self.definitionChanges.setDirty(True)
stack.definitionChanges.removeInstance(key, postpone_emit = True)
# Make sure the material diameter is up to date for the extruder stack.
if key == "material_diameter":
from cura.CuraApplication import CuraApplication
machine_manager = CuraApplication.getInstance().getMachineManager()
position = self.getMetaDataEntry("position", "0")
func = lambda p = position: CuraApplication.getInstance().getExtruderManager().updateMaterialForDiameter(p)
machine_manager.machine_extruder_material_update_dict[stack.getId()].append(func)
# NOTE: We cannot remove the setting from the global stack's definition changes container because for
# material diameter, it needs to be applied to all extruders, but here we don't know how many extruders
# a machine actually has and how many extruders has already been loaded for that machine, so we have to
# keep this setting for any remaining extruders that haven't been loaded yet.
#
# Those settings will be removed in ExtruderManager which knows all those info.
@override(ContainerStack)
def getNextStack(self) -> Optional["GlobalStack"]:

View file

@ -1,6 +1,8 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from collections import defaultdict
import threading
from typing import Any, Dict, Optional
from PyQt5.QtCore import pyqtProperty
@ -30,7 +32,8 @@ class GlobalStack(CuraContainerStack):
# This property is used to track which settings we are calculating the "resolve" for
# and if so, to bypass the resolve to prevent an infinite recursion that would occur
# if the resolve function tried to access the same property it is a resolve for.
self._resolving_settings = set()
# Per thread we have our own resolving_settings, or strange things sometimes occur.
self._resolving_settings = defaultdict(set) # keys are thread names
## Get the list of extruders of this stack.
#
@ -91,9 +94,10 @@ class GlobalStack(CuraContainerStack):
# Handle the "resolve" property.
if self._shouldResolve(key, property_name, context):
self._resolving_settings.add(key)
current_thread = threading.current_thread()
self._resolving_settings[current_thread.name].add(key)
resolve = super().getProperty(key, "resolve", context)
self._resolving_settings.remove(key)
self._resolving_settings[current_thread.name].remove(key)
if resolve is not None:
return resolve
@ -145,7 +149,8 @@ class GlobalStack(CuraContainerStack):
# Do not try to resolve anything but the "value" property
return False
if key in self._resolving_settings:
current_thread = threading.current_thread()
if key in self._resolving_settings[current_thread.name]:
# To prevent infinite recursion, if getProperty is called with the same key as
# we are already trying to resolve, we should not try to resolve again. Since
# this can happen multiple times when trying to resolve a value, we need to

View file

@ -1,11 +1,16 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import collections
import time
#Type hinting.
from typing import Union, List, Dict
from typing import Union, List, Dict, TYPE_CHECKING, Optional
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Signal import Signal
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, QTimer
import UM.FlameProfiler
from UM.FlameProfiler import pyqtSlot
from PyQt5.QtWidgets import QMessageBox
from UM import Util
@ -21,7 +26,7 @@ from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.SettingFunction import SettingFunction
from UM.Signal import postponeSignals, CompressTechnique
import UM.FlameProfiler
from cura.QualityManager import QualityManager
from cura.PrinterOutputDevice import PrinterOutputDevice
@ -33,7 +38,6 @@ from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
from cura.Settings.ProfilesModel import ProfilesModel
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from UM.Settings.DefinitionContainer import DefinitionContainer
@ -48,10 +52,13 @@ class MachineManager(QObject):
self._active_container_stack = None # type: CuraContainerStack
self._global_container_stack = None # type: GlobalStack
self.machine_extruder_material_update_dict = collections.defaultdict(list)
# Used to store the new containers until after confirming the dialog
self._new_variant_container = None
self._new_material_container = None
self._new_quality_containers = []
self._new_variant_container = None # type: Optional[InstanceContainer]
self._new_buildplate_container = None # type: Optional[InstanceContainer]
self._new_material_container = None # type: Optional[InstanceContainer]
self._new_quality_containers = [] # type: List[Dict]
self._error_check_timer = QTimer()
self._error_check_timer.setInterval(250)
@ -72,8 +79,9 @@ class MachineManager(QObject):
self.globalContainerChanged.connect(self.activeVariantChanged)
self.globalContainerChanged.connect(self.activeQualityChanged)
self._stacks_have_errors = None
self._stacks_have_errors = None # type:Optional[bool]
self._empty_definition_changes_container = ContainerRegistry.getInstance().findContainers(id = "empty_definition_changes")[0]
self._empty_variant_container = ContainerRegistry.getInstance().findContainers(id = "empty_variant")[0]
self._empty_material_container = ContainerRegistry.getInstance().findContainers(id = "empty_material")[0]
self._empty_quality_container = ContainerRegistry.getInstance().findContainers(id = "empty_quality")[0]
@ -144,8 +152,7 @@ class MachineManager(QObject):
printer_output_device.hotendIdChanged.disconnect(self._onHotendIdChanged)
printer_output_device.materialIdChanged.disconnect(self._onMaterialIdChanged)
self._printer_output_devices.clear()
self._printer_output_devices = []
for printer_output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
if isinstance(printer_output_device, PrinterOutputDevice):
self._printer_output_devices.append(printer_output_device)
@ -158,6 +165,10 @@ class MachineManager(QObject):
def newVariant(self):
return self._new_variant_container
@property
def newBuildplate(self):
return self._new_buildplate_container
@property
def newMaterial(self):
return self._new_material_container
@ -170,60 +181,72 @@ class MachineManager(QObject):
def totalNumberOfSettings(self) -> int:
return len(ContainerRegistry.getInstance().findDefinitionContainers(id = "fdmprinter")[0].getAllKeys())
def _onHotendIdChanged(self, index: Union[str, int], hotend_id: str) -> None:
if not self._global_container_stack:
def _onHotendIdChanged(self) -> None:
if not self._global_container_stack or not self._printer_output_devices:
return
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type = "variant", definition = self._global_container_stack.definition.getId(), name = hotend_id)
if containers: # New material ID is known
extruder_manager = ExtruderManager.getInstance()
active_printer_model = self._printer_output_devices[0].activePrinter
if not active_printer_model:
return
change_found = False
machine_id = self.activeMachineId
extruders = extruder_manager.getMachineExtruders(machine_id)
matching_extruder = None
for extruder in extruders:
if str(index) == extruder.getMetaDataEntry("position"):
matching_extruder = extruder
break
if matching_extruder and matching_extruder.variant.getName() != hotend_id:
# Save the material that needs to be changed. Multiple changes will be handled by the callback.
self._auto_hotends_changed[str(index)] = containers[0]["id"]
self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback)
else:
Logger.log("w", "No variant found for printer definition %s with id %s" % (self._global_container_stack.definition.getId(), hotend_id))
extruders = sorted(ExtruderManager.getInstance().getMachineExtruders(machine_id),
key=lambda k: k.getMetaDataEntry("position"))
def _onMaterialIdChanged(self, index: Union[str, int], material_id: str):
if not self._global_container_stack:
for extruder_model, extruder in zip(active_printer_model.extruders, extruders):
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type="variant",
definition=self._global_container_stack.definition.getId(),
name=extruder_model.hotendID)
if containers:
# The hotend ID is known.
machine_id = self.activeMachineId
if extruder.variant.getName() != extruder_model.hotendID:
change_found = True
self._auto_hotends_changed[extruder.getMetaDataEntry("position")] = containers[0]["id"]
if change_found:
# A change was found, let the output device handle this.
self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback)
def _onMaterialIdChanged(self) -> None:
if not self._global_container_stack or not self._printer_output_devices:
return
definition_id = "fdmprinter"
if self._global_container_stack.getMetaDataEntry("has_machine_materials", False):
definition_id = self.activeQualityDefinitionId
extruder_manager = ExtruderManager.getInstance()
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type = "material", definition = definition_id, GUID = material_id)
if containers: # New material ID is known
extruders = list(extruder_manager.getMachineExtruders(self.activeMachineId))
matching_extruder = None
for extruder in extruders:
if str(index) == extruder.getMetaDataEntry("position"):
matching_extruder = extruder
break
active_printer_model = self._printer_output_devices[0].activePrinter
if not active_printer_model:
return
if matching_extruder and matching_extruder.material.getMetaDataEntry("GUID") != material_id:
# Save the material that needs to be changed. Multiple changes will be handled by the callback.
if self._global_container_stack.definition.getMetaDataEntry("has_variants") and matching_extruder.variant:
variant_id = self.getQualityVariantId(self._global_container_stack.definition, matching_extruder.variant)
change_found = False
machine_id = self.activeMachineId
extruders = sorted(ExtruderManager.getInstance().getMachineExtruders(machine_id),
key=lambda k: k.getMetaDataEntry("position"))
for extruder_model, extruder in zip(active_printer_model.extruders, extruders):
if extruder_model.activeMaterial is None:
continue
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type="material",
definition=self._global_container_stack.definition.getId(),
GUID=extruder_model.activeMaterial.guid)
if containers:
# The material is known.
if extruder.material.getMetaDataEntry("GUID") != extruder_model.activeMaterial.guid:
change_found = True
if self._global_container_stack.definition.getMetaDataEntry("has_variants") and extruder.variant:
variant_id = self.getQualityVariantId(self._global_container_stack.definition,
extruder.variant)
for container in containers:
if container.get("variant") == variant_id:
self._auto_materials_changed[str(index)] = container["id"]
self._auto_materials_changed[extruder.getMetaDataEntry("position")] = container["id"]
break
else:
# Just use the first result we found.
self._auto_materials_changed[str(index)] = containers[0]["id"]
self._auto_materials_changed[extruder.getMetaDataEntry("position")] = containers[0]["id"]
if change_found:
# A change was found, let the output device handle this.
self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback)
else:
Logger.log("w", "No material definition found for printer definition %s and GUID %s" % (definition_id, material_id))
def _materialHotendChangedCallback(self, button):
def _materialHotendChangedCallback(self, button) -> None:
if button == QMessageBox.No:
self._auto_materials_changed = {}
self._auto_hotends_changed = {}
@ -231,7 +254,7 @@ class MachineManager(QObject):
self._autoUpdateMaterials()
self._autoUpdateHotends()
def _autoUpdateMaterials(self):
def _autoUpdateMaterials(self) -> None:
extruder_manager = ExtruderManager.getInstance()
for position in self._auto_materials_changed:
material_id = self._auto_materials_changed[position]
@ -249,7 +272,7 @@ class MachineManager(QObject):
extruder_manager.setActiveExtruderIndex(old_index)
self._auto_materials_changed = {} # Processed all of them now.
def _autoUpdateHotends(self):
def _autoUpdateHotends(self) -> None:
extruder_manager = ExtruderManager.getInstance()
for position in self._auto_hotends_changed:
hotend_id = self._auto_hotends_changed[position]
@ -266,7 +289,7 @@ class MachineManager(QObject):
extruder_manager.setActiveExtruderIndex(old_index)
self._auto_hotends_changed = {} # Processed all of them now.
def _onGlobalContainerChanged(self):
def _onGlobalContainerChanged(self) -> None:
if self._global_container_stack:
try:
self._global_container_stack.nameChanged.disconnect(self._onMachineNameChanged)
@ -285,7 +308,7 @@ class MachineManager(QObject):
extruder_stack.propertyChanged.disconnect(self._onPropertyChanged)
extruder_stack.containersChanged.disconnect(self._onInstanceContainersChanged)
# update the local global container stack reference
# Update the local global container stack reference
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
self.globalContainerChanged.emit()
@ -298,9 +321,10 @@ class MachineManager(QObject):
self._global_container_stack.containersChanged.connect(self._onInstanceContainersChanged)
self._global_container_stack.propertyChanged.connect(self._onPropertyChanged)
# set the global variant to empty as we now use the extruder stack at all times - CURA-4482
# Global stack can have only a variant if it is a buildplate
global_variant = self._global_container_stack.variant
if global_variant != self._empty_variant_container:
if global_variant.getMetaDataEntry("hardware_type") != "buildplate":
self._global_container_stack.setVariant(self._empty_variant_container)
# set the global material to empty as we now use the extruder stack at all times - CURA-4482
@ -313,17 +337,22 @@ class MachineManager(QObject):
extruder_stack.propertyChanged.connect(self._onPropertyChanged)
extruder_stack.containersChanged.connect(self._onInstanceContainersChanged)
if self._global_container_stack.getId() in self.machine_extruder_material_update_dict:
for func in self.machine_extruder_material_update_dict[self._global_container_stack.getId()]:
Application.getInstance().callLater(func)
del self.machine_extruder_material_update_dict[self._global_container_stack.getId()]
self._error_check_timer.start()
## Update self._stacks_valid according to _checkStacksForErrors and emit if change.
def _updateStacksHaveErrors(self):
def _updateStacksHaveErrors(self) -> None:
old_stacks_have_errors = self._stacks_have_errors
self._stacks_have_errors = self._checkStacksHaveErrors()
if old_stacks_have_errors != self._stacks_have_errors:
self.stacksValidationChanged.emit()
Application.getInstance().stacksValidationFinished.emit()
def _onActiveExtruderStackChanged(self):
def _onActiveExtruderStackChanged(self) -> None:
self.blurSettings.emit() # Ensure no-one has focus.
old_active_container_stack = self._active_container_stack
@ -336,17 +365,16 @@ class MachineManager(QObject):
# on _active_container_stack. If it changes, then the properties change.
self.activeQualityChanged.emit()
def __emitChangedSignals(self):
def __emitChangedSignals(self) -> None:
self.activeQualityChanged.emit()
self.activeVariantChanged.emit()
self.activeMaterialChanged.emit()
self._updateStacksHaveErrors() # Prevents unwanted re-slices after changing machine
self._error_check_timer.start()
def _onProfilesModelChanged(self, *args):
def _onProfilesModelChanged(self, *args) -> None:
self.__emitChangedSignals()
def _onInstanceContainersChanged(self, container):
def _onInstanceContainersChanged(self, container) -> None:
# This should not trigger the ProfilesModel to be created, or there will be an infinite recursion
if not self._connected_to_profiles_model and ProfilesModel.hasInstance():
# This triggers updating the qualityModel in SidebarSimple whenever ProfilesModel is updated
@ -356,7 +384,7 @@ class MachineManager(QObject):
self._instance_container_timer.start()
def _onPropertyChanged(self, key: str, property_name: str):
def _onPropertyChanged(self, key: str, property_name: str) -> None:
if property_name == "value":
# Notify UI items, such as the "changed" star in profile pull down menu.
self.activeStackValueChanged.emit()
@ -387,20 +415,33 @@ class MachineManager(QObject):
Logger.log("w", "Failed creating a new machine!")
def _checkStacksHaveErrors(self) -> bool:
time_start = time.time()
if self._global_container_stack is None: #No active machine.
return False
if self._global_container_stack.hasErrors():
return True
for stack in ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()):
if stack.hasErrors():
Logger.log("d", "Checking global stack for errors took %0.2f s and we found and error" % (time.time() - time_start))
return True
# Not a very pretty solution, but the extruder manager doesn't really know how many extruders there are
machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
extruder_stacks = ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())
count = 1 # we start with the global stack
for stack in extruder_stacks:
md = stack.getMetaData()
if "position" in md and int(md["position"]) >= machine_extruder_count:
continue
count += 1
if stack.hasErrors():
Logger.log("d", "Checking %s stacks for errors took %.2f s and we found an error in stack [%s]" % (count, time.time() - time_start, str(stack)))
return True
Logger.log("d", "Checking %s stacks for errors took %.2f s" % (count, time.time() - time_start))
return False
## Remove all instances from the top instanceContainer (effectively removing all user-changed settings)
@pyqtSlot()
def clearUserSettings(self):
def clearUserSettings(self) -> None:
if not self._active_container_stack:
return
@ -438,7 +479,7 @@ class MachineManager(QObject):
## Delete a user setting from the global stack and all extruder stacks.
# \param key \type{str} the name of the key to delete
@pyqtSlot(str)
def clearUserSettingAllCurrentStacks(self, key: str):
def clearUserSettingAllCurrentStacks(self, key: str) -> None:
if not self._global_container_stack:
return
@ -664,6 +705,14 @@ class MachineManager(QObject):
return quality.getId()
return ""
@pyqtProperty(str, notify=activeVariantChanged)
def globalVariantId(self) -> str:
if self._global_container_stack:
variant = self._global_container_stack.variant
if variant and not isinstance(variant, type(self._empty_variant_container)):
return variant.getId()
return ""
@pyqtProperty(str, notify = activeQualityChanged)
def activeQualityType(self) -> str:
if self._active_container_stack:
@ -739,7 +788,7 @@ class MachineManager(QObject):
## Set the active material by switching out a container
# Depending on from/to material+current variant, a quality profile is chosen and set.
@pyqtSlot(str)
def setActiveMaterial(self, material_id: str):
def setActiveMaterial(self, material_id: str, always_discard_changes = False):
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
containers = ContainerRegistry.getInstance().findInstanceContainers(id = material_id)
if not containers or not self._active_container_stack:
@ -821,10 +870,10 @@ class MachineManager(QObject):
if not old_quality_changes:
new_quality_id = candidate_quality.getId()
self.setActiveQuality(new_quality_id)
self.setActiveQuality(new_quality_id, always_discard_changes = always_discard_changes)
@pyqtSlot(str)
def setActiveVariant(self, variant_id: str):
def setActiveVariant(self, variant_id: str, always_discard_changes = False):
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
containers = ContainerRegistry.getInstance().findInstanceContainers(id = variant_id)
if not containers or not self._active_container_stack:
@ -840,17 +889,37 @@ class MachineManager(QObject):
if old_material:
preferred_material_name = old_material.getName()
preferred_material_id = self._updateMaterialContainer(self._global_container_stack.definition, self._global_container_stack, containers[0], preferred_material_name).id
self.setActiveMaterial(preferred_material_id)
self.setActiveMaterial(preferred_material_id, always_discard_changes = always_discard_changes)
else:
Logger.log("w", "While trying to set the active variant, no variant was found to replace.")
@pyqtSlot(str)
def setActiveVariantBuildplate(self, variant_buildplate_id: str):
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
containers = ContainerRegistry.getInstance().findInstanceContainers(id = variant_buildplate_id)
if not containers or not self._global_container_stack:
return
Logger.log("d", "Attempting to change the active buildplate to %s", variant_buildplate_id)
old_buildplate = self._global_container_stack.variant
if old_buildplate:
self.blurSettings.emit()
self._new_buildplate_container = containers[0] # self._active_container_stack will be updated with a delay
Logger.log("d", "Active buildplate changed to {active_variant_buildplate_id}".format(active_variant_buildplate_id = containers[0].getId()))
# Force set the active quality as it is so the values are updated
self.setActiveMaterial(self._active_container_stack.material.getId())
else:
Logger.log("w", "While trying to set the active buildplate, no buildplate was found to replace.")
## set the active quality
# \param quality_id The quality_id of either a quality or a quality_changes
@pyqtSlot(str)
def setActiveQuality(self, quality_id: str):
def setActiveQuality(self, quality_id: str, always_discard_changes = False):
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
self.blurSettings.emit()
Logger.log("d", "Attempting to change the active quality to %s", quality_id)
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = quality_id)
if not containers or not self._global_container_stack:
return
@ -905,11 +974,13 @@ class MachineManager(QObject):
"quality_changes": stack_quality_changes
})
Logger.log("d", "Active quality changed")
# show the keep/discard dialog after the containers have been switched. Otherwise, the default values on
# the dialog will be the those before the switching.
self._executeDelayedActiveContainerStackChanges()
if self.hasUserSettings and Preferences.getInstance().getValue("cura/active_mode") == 1:
if self.hasUserSettings and Preferences.getInstance().getValue("cura/active_mode") == 1 and not always_discard_changes:
Application.getInstance().discardOrKeepProfileChanges()
## Used to update material and variant in the active container stack with a delay.
@ -917,10 +988,17 @@ class MachineManager(QObject):
# before the user decided to keep or discard any of their changes using the dialog.
# The Application.onDiscardOrKeepProfileChangesClosed signal triggers this method.
def _executeDelayedActiveContainerStackChanges(self):
Logger.log("d", "Applying configuration changes...")
if self._new_variant_container is not None:
self._active_container_stack.variant = self._new_variant_container
self._new_variant_container = None
if self._new_buildplate_container is not None:
self._global_container_stack.variant = self._new_buildplate_container
self._new_buildplate_container = None
if self._new_material_container is not None:
self._active_container_stack.material = self._new_material_container
self._new_material_container = None
@ -937,10 +1015,13 @@ class MachineManager(QObject):
self._new_quality_containers.clear()
Logger.log("d", "New configuration applied")
## Cancel set changes for material and variant in the active container stack.
# Used for ignoring any changes when switching between printers (setActiveMachine)
def _cancelDelayedActiveContainerStackChanges(self):
self._new_material_container = None
self._new_buildplate_container = None
self._new_variant_container = None
## Determine the quality and quality changes settings for the current machine for a quality name.
@ -1105,6 +1186,15 @@ class MachineManager(QObject):
return ""
@pyqtProperty(str, notify = activeVariantChanged)
def activeVariantBuildplateName(self) -> str:
if self._global_container_stack:
variant = self._global_container_stack.variant
if variant:
return variant.getName()
return ""
@pyqtProperty(str, notify = globalContainerChanged)
def activeDefinitionId(self) -> str:
if self._global_container_stack:
@ -1202,7 +1292,6 @@ class MachineManager(QObject):
def hasMaterials(self) -> bool:
if self._global_container_stack:
return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False))
return False
@pyqtProperty(bool, notify = globalContainerChanged)
@ -1211,6 +1300,53 @@ class MachineManager(QObject):
return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_variants", False))
return False
@pyqtProperty(bool, notify = globalContainerChanged)
def hasVariantBuildplates(self) -> bool:
if self._global_container_stack:
return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_variant_buildplates", False))
return False
## The selected buildplate is compatible if it is compatible with all the materials in all the extruders
@pyqtProperty(bool, notify = activeMaterialChanged)
def variantBuildplateCompatible(self) -> bool:
if not self._global_container_stack:
return True
buildplate_compatible = True # It is compatible by default
extruder_stacks = self._global_container_stack.extruders.values()
for stack in extruder_stacks:
material_container = stack.material
if material_container == self._empty_material_container:
continue
if material_container.getMetaDataEntry("buildplate_compatible"):
buildplate_compatible = buildplate_compatible and material_container.getMetaDataEntry("buildplate_compatible")[self.activeVariantBuildplateName]
return buildplate_compatible
## The selected buildplate is usable if it is usable for all materials OR it is compatible for one but not compatible
# for the other material but the buildplate is still usable
@pyqtProperty(bool, notify = activeMaterialChanged)
def variantBuildplateUsable(self) -> bool:
if not self._global_container_stack:
return True
# Here the next formula is being calculated:
# result = (not (material_left_compatible and material_right_compatible)) and
# (material_left_compatible or material_left_usable) and
# (material_right_compatible or material_right_usable)
result = not self.variantBuildplateCompatible
extruder_stacks = self._global_container_stack.extruders.values()
for stack in extruder_stacks:
material_container = stack.material
if material_container == self._empty_material_container:
continue
buildplate_compatible = material_container.getMetaDataEntry("buildplate_compatible")[self.activeVariantBuildplateName] if material_container.getMetaDataEntry("buildplate_compatible") else True
buildplate_usable = material_container.getMetaDataEntry("buildplate_recommended")[self.activeVariantBuildplateName] if material_container.getMetaDataEntry("buildplate_recommended") else True
result = result and (buildplate_compatible or buildplate_usable)
return result
## Property to indicate if a machine has "specialized" material profiles.
# Some machines have their own material profiles that "override" the default catch all profiles.
@pyqtProperty(bool, notify = globalContainerChanged)
@ -1236,6 +1372,68 @@ class MachineManager(QObject):
if containers:
return containers[0].definition.getId()
## Set the amount of extruders on the active machine (global stack)
# \param extruder_count int the number of extruders to set
def setActiveMachineExtruderCount(self, extruder_count):
extruder_manager = Application.getInstance().getExtruderManager()
definition_changes_container = self._global_container_stack.definitionChanges
if not self._global_container_stack or definition_changes_container == self._empty_definition_changes_container:
return
previous_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
if extruder_count == previous_extruder_count:
return
# reset all extruder number settings whose value is no longer valid
for setting_instance in self._global_container_stack.userChanges.findInstances():
setting_key = setting_instance.definition.key
if not self._global_container_stack.getProperty(setting_key, "type") in ("extruder", "optional_extruder"):
continue
old_value = int(self._global_container_stack.userChanges.getProperty(setting_key, "value"))
if old_value >= extruder_count:
self._global_container_stack.userChanges.removeInstance(setting_key)
Logger.log("d", "Reset [%s] because its old value [%s] is no longer valid ", setting_key, old_value)
# Check to see if any objects are set to print with an extruder that will no longer exist
root_node = Application.getInstance().getController().getScene().getRoot()
for node in DepthFirstIterator(root_node):
if node.getMeshData():
extruder_nr = node.callDecoration("getActiveExtruderPosition")
if extruder_nr is not None and int(extruder_nr) > extruder_count - 1:
node.callDecoration("setActiveExtruder", extruder_manager.getExtruderStack(extruder_count - 1).getId())
definition_changes_container.setProperty("machine_extruder_count", "value", extruder_count)
# Make sure one of the extruder stacks is active
extruder_manager.setActiveExtruderIndex(0)
# Move settable_per_extruder values out of the global container
# After CURA-4482 this should not be the case anymore, but we still want to support older project files.
global_user_container = self._global_container_stack.getTop()
# Make sure extruder_stacks exists
extruder_stacks = []
if previous_extruder_count == 1:
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
global_user_container = self._global_container_stack.getTop()
for setting_instance in global_user_container.findInstances():
setting_key = setting_instance.definition.key
settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder")
if settable_per_extruder:
limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder"))
extruder_stack = extruder_stacks[max(0, limit_to_extruder)]
extruder_stack.getTop().setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value"))
global_user_container.removeInstance(setting_key)
# Signal that the global stack has changed
Application.getInstance().globalContainerStackChanged.emit()
@staticmethod
def createMachineManager():
return MachineManager()

View file

@ -36,6 +36,8 @@ class ProfilesModel(InstanceContainersModel):
Application.getInstance().getMachineManager().activeStackChanged.connect(self._update)
Application.getInstance().getMachineManager().activeMaterialChanged.connect(self._update)
self._empty_quality = ContainerRegistry.getInstance().findContainers(id = "empty_quality")[0]
# Factory function, used by QML
@staticmethod
def createProfilesModel(engine, js_engine):
@ -85,11 +87,8 @@ class ProfilesModel(InstanceContainersModel):
if quality.getMetaDataEntry("quality_type") not in quality_type_set:
result.append(quality)
# if still profiles are found, add a single empty_quality ("Not supported") instance to the drop down list
if len(result) == 0:
# If not qualities are found we dynamically create a not supported container for this machine + material combination
not_supported_container = ContainerRegistry.getInstance().findContainers(id = "empty_quality")[0]
result.append(not_supported_container)
if len(result) > 1 and self._empty_quality in result:
result.remove(self._empty_quality)
return {item.getId(): item for item in result}, {} #Only return true profiles for now, no metadata. The quality manager is not able to get only metadata yet.
@ -114,7 +113,6 @@ class ProfilesModel(InstanceContainersModel):
# active machine and material, and later yield the right ones.
tmp_all_quality_items = OrderedDict()
for item in super()._recomputeItems():
profiles = container_registry.findContainersMetadata(id = item["id"])
if not profiles or "quality_type" not in profiles[0]:
quality_type = ""

View file

@ -1,17 +1,21 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.QualityManager import QualityManager
from cura.Settings.ProfilesModel import ProfilesModel
from cura.Settings.ExtruderManager import ExtruderManager
## QML Model for listing the current list of valid quality and quality changes profiles.
#
class QualityAndUserProfilesModel(ProfilesModel):
def __init__(self, parent = None):
super().__init__(parent)
self._empty_quality = ContainerRegistry.getInstance().findInstanceContainers(id = "empty_quality")[0]
## Fetch the list of containers to display.
#
# See UM.Settings.Models.InstanceContainersModel._fetchInstanceContainers().
@ -35,6 +39,8 @@ class QualityAndUserProfilesModel(ProfilesModel):
# Filter the quality_change by the list of available quality_types
quality_type_set = set([x.getMetaDataEntry("quality_type") for x in quality_list])
# Also show custom profiles based on "Not Supported" quality profile
quality_type_set.add(self._empty_quality.getMetaDataEntry("quality_type"))
filtered_quality_changes = {qc.getId(): qc for qc in quality_changes_list if
qc.getMetaDataEntry("quality_type") in quality_type_set and
qc.getMetaDataEntry("extruder") is not None and
@ -42,5 +48,7 @@ class QualityAndUserProfilesModel(ProfilesModel):
qc.getMetaDataEntry("extruder") == active_extruder.definition.getId())}
result = filtered_quality_changes
result.update({q.getId():q for q in quality_list})
for q in quality_list:
if q.getId() != "empty_quality":
result[q.getId()] = q
return result, {} #Only return true profiles for now, no metadata. The quality manager is not able to get only metadata yet.

View file

@ -1,8 +1,6 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import collections
from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt
from UM.Logger import Logger
@ -42,6 +40,8 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
self.addRoleName(self.UserValueRole, "user_value")
self.addRoleName(self.CategoryRole, "category")
self._empty_quality = self._container_registry.findInstanceContainers(id = "empty_quality")[0]
def setExtruderId(self, extruder_id):
if extruder_id != self._extruder_id:
self._extruder_id = extruder_id
@ -107,6 +107,9 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
else:
quality_changes_container = containers[0]
if quality_changes_container.getMetaDataEntry("quality_type") == self._empty_quality.getMetaDataEntry("quality_type"):
quality_container = self._empty_quality
else:
criteria = {
"type": "quality",
"quality_type": quality_changes_container.getMetaDataEntry("quality_type"),
@ -117,9 +120,14 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
if not quality_container:
Logger.log("w", "Could not find a quality container matching quality changes %s", quality_changes_container.getId())
return
quality_container = quality_container[0]
quality_type = quality_container.getMetaDataEntry("quality_type")
if quality_type == "not_supported":
containers = []
else:
definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(quality_container.getDefinition())
definition = quality_container.getDefinition()
@ -163,6 +171,9 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
return
if quality_changes_container:
if quality_type == "not_supported":
criteria = {"type": "quality_changes", "quality_type": quality_type, "name": quality_changes_container.getName()}
else:
criteria = {"type": "quality_changes", "quality_type": quality_type, "definition": definition_id, "name": quality_changes_container.getName()}
if self._extruder_definition_id != "":
extruder_definitions = self._container_registry.findDefinitionContainers(id = self._extruder_definition_id)
@ -177,7 +188,6 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
containers.extend(changes)
global_container_stack = Application.getInstance().getGlobalContainerStack()
is_multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
current_category = ""
for definition in definition_container.findDefinitions():
@ -213,7 +223,6 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
if profile_value is None and user_value is None:
continue
if is_multi_extrusion:
settable_per_extruder = global_container_stack.getProperty(definition.key, "settable_per_extruder")
# If a setting is not settable per extruder (global) and we're looking at an extruder tab, don't show this value.
if self._extruder_id != "" and not settable_per_extruder:

View file

@ -2,6 +2,8 @@
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.QualityManager import QualityManager
from cura.Settings.ProfilesModel import ProfilesModel
from cura.Settings.ExtruderManager import ExtruderManager
@ -22,6 +24,8 @@ class UserProfilesModel(ProfilesModel):
for material in self.__current_materials:
material.metaDataChanged.connect(self._onContainerChanged)
self._empty_quality = ContainerRegistry.getInstance().findContainers(id = "empty_quality")[0]
## Fetch the list of containers to display.
#
# See UM.Settings.Models.InstanceContainersModel._fetchInstanceContainers().
@ -45,6 +49,7 @@ class UserProfilesModel(ProfilesModel):
# Filter the quality_change by the list of available quality_types
quality_type_set = set([x.getMetaDataEntry("quality_type") for x in quality_list])
quality_type_set.add(self._empty_quality.getMetaDataEntry("quality_type"))
filtered_quality_changes = {qc.getId():qc for qc in quality_changes_list if
qc.getMetaDataEntry("quality_type") in quality_type_set and

115
cura/Snapshot.py Normal file
View file

@ -0,0 +1,115 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import numpy
from PyQt5 import QtCore
from PyQt5.QtGui import QImage
from cura.PreviewPass import PreviewPass
from cura.Scene import ConvexHullNode
from UM.Application import Application
from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Matrix import Matrix
from UM.Math.Vector import Vector
from UM.Mesh.MeshData import transformVertices
from UM.Scene.Camera import Camera
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
class Snapshot:
@staticmethod
def getImageBoundaries(image: QImage):
# Look at the resulting image to get a good crop.
# Get the pixels as byte array
pixel_array = image.bits().asarray(image.byteCount())
width, height = image.width(), image.height()
# Convert to numpy array, assume it's 32 bit (it should always be)
pixels = numpy.frombuffer(pixel_array, dtype=numpy.uint8).reshape([height, width, 4])
# Find indices of non zero pixels
nonzero_pixels = numpy.nonzero(pixels)
min_y, min_x, min_a_ = numpy.amin(nonzero_pixels, axis=1)
max_y, max_x, max_a_ = numpy.amax(nonzero_pixels, axis=1)
return min_x, max_x, min_y, max_y
## Return a QImage of the scene
# Uses PreviewPass that leaves out some elements
# Aspect ratio assumes a square
@staticmethod
def snapshot(width = 300, height = 300):
scene = Application.getInstance().getController().getScene()
active_camera = scene.getActiveCamera()
render_width, render_height = active_camera.getWindowSize()
render_width = int(render_width)
render_height = int(render_height)
preview_pass = PreviewPass(render_width, render_height)
root = scene.getRoot()
camera = Camera("snapshot", root)
# determine zoom and look at
bbox = None
for node in DepthFirstIterator(root):
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
if bbox is None:
bbox = node.getBoundingBox()
else:
bbox = bbox + node.getBoundingBox()
if bbox is None:
bbox = AxisAlignedBox()
look_at = bbox.center
# guessed size so the objects are hopefully big
size = max(bbox.width, bbox.height, bbox.depth * 0.5)
# Looking from this direction (x, y, z) in OGL coordinates
looking_from_offset = Vector(1, 1, 2)
if size > 0:
# determine the watch distance depending on the size
looking_from_offset = looking_from_offset * size * 1.3
camera.setPosition(look_at + looking_from_offset)
camera.lookAt(look_at)
satisfied = False
size = None
fovy = 30
while not satisfied:
if size is not None:
satisfied = True # always be satisfied after second try
projection_matrix = Matrix()
# Somehow the aspect ratio is also influenced in reverse by the screen width/height
# So you have to set it to render_width/render_height to get 1
projection_matrix.setPerspective(fovy, render_width / render_height, 1, 500)
camera.setProjectionMatrix(projection_matrix)
preview_pass.setCamera(camera)
preview_pass.render()
pixel_output = preview_pass.getOutput()
min_x, max_x, min_y, max_y = Snapshot.getImageBoundaries(pixel_output)
size = max((max_x - min_x) / render_width, (max_y - min_y) / render_height)
if size > 0.5 or satisfied:
satisfied = True
else:
# make it big and allow for some empty space around
fovy *= 0.5 # strangely enough this messes up the aspect ratio: fovy *= size * 1.1
# make it a square
if max_x - min_x >= max_y - min_y:
# make y bigger
min_y, max_y = int((max_y + min_y) / 2 - (max_x - min_x) / 2), int((max_y + min_y) / 2 + (max_x - min_x) / 2)
else:
# make x bigger
min_x, max_x = int((max_x + min_x) / 2 - (max_y - min_y) / 2), int((max_x + min_x) / 2 + (max_y - min_y) / 2)
cropped_image = pixel_output.copy(min_x, min_y, max_x - min_x, max_y - min_y)
# Scale it to the correct size
scaled_image = cropped_image.scaled(
width, height,
aspectRatioMode = QtCore.Qt.IgnoreAspectRatio,
transformMode = QtCore.Qt.SmoothTransformation)
return scaled_image

View file

@ -16,6 +16,12 @@ parser.add_argument('--debug',
default = False,
help = "Turn on the debug mode by setting this option."
)
parser.add_argument('--trigger-early-crash',
dest = 'trigger_early_crash',
action = 'store_true',
default = False,
help = "FOR TESTING ONLY. Trigger an early crash to show the crash dialog."
)
known_args = vars(parser.parse_known_args()[0])
if not known_args["debug"]:
@ -30,8 +36,8 @@ if not known_args["debug"]:
if hasattr(sys, "frozen"):
dirpath = get_cura_dir_path()
os.makedirs(dirpath, exist_ok = True)
sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w")
sys.stderr = open(os.path.join(dirpath, "stderr.log"), "w")
sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w", encoding = "utf-8")
sys.stderr = open(os.path.join(dirpath, "stderr.log"), "w", encoding = "utf-8")
import platform
import faulthandler
@ -40,7 +46,7 @@ import faulthandler
if Platform.isLinux(): # Needed for platform.linux_distribution, which is not available on Windows and OSX
# For Ubuntu: https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
linux_distro_name = platform.linux_distribution()[0].lower()
if linux_distro_name in ("debian", "ubuntu", "linuxmint", "fedora"): # TODO: Needs a "if X11_GFX == 'nvidia'" here. The workaround is only needed on Ubuntu+NVidia drivers. Other drivers are not affected, but fine with this fix.
# TODO: Needs a "if X11_GFX == 'nvidia'" here. The workaround is only needed on Ubuntu+NVidia drivers. Other drivers are not affected, but fine with this fix.
import ctypes
from ctypes.util import find_library
libGL = find_library("GL")
@ -71,8 +77,45 @@ if "PYTHONPATH" in os.environ.keys(): # If PYTHONPATH is u
def exceptHook(hook_type, value, traceback):
from cura.CrashHandler import CrashHandler
_crash_handler = CrashHandler(hook_type, value, traceback)
from cura.CuraApplication import CuraApplication
has_started = False
if CuraApplication.Created:
has_started = CuraApplication.getInstance().started
#
# When the exception hook is triggered, the QApplication may not have been initialized yet. In this case, we don't
# have an QApplication to handle the event loop, which is required by the Crash Dialog.
# The flag "CuraApplication.Created" is set to True when CuraApplication finishes its constructor call.
#
# Before the "started" flag is set to True, the Qt event loop has not started yet. The event loop is a blocking
# call to the QApplication.exec_(). In this case, we need to:
# 1. Remove all scheduled events so no more unnecessary events will be processed, such as loading the main dialog,
# loading the machine, etc.
# 2. Start the Qt event loop with exec_() and show the Crash Dialog.
#
# If the application has finished its initialization and was running fine, and then something causes a crash,
# we run the old routine to show the Crash Dialog.
#
from PyQt5.Qt import QApplication
if CuraApplication.Created:
_crash_handler = CrashHandler(hook_type, value, traceback, has_started)
if CuraApplication.splash is not None:
CuraApplication.splash.close()
if not has_started:
CuraApplication.getInstance().removePostedEvents(None)
_crash_handler.early_crash_dialog.show()
sys.exit(CuraApplication.getInstance().exec_())
else:
_crash_handler.show()
else:
application = QApplication(sys.argv)
application.removePostedEvents(None)
_crash_handler = CrashHandler(hook_type, value, traceback, has_started)
# This means the QtApplication could be created and so the splash screen. Then Cura closes it
if CuraApplication.splash is not None:
CuraApplication.splash.close()
_crash_handler.early_crash_dialog.show()
sys.exit(application.exec_())
if not known_args["debug"]:
sys.excepthook = exceptHook

View file

@ -1,4 +1,4 @@
# Copyright (c) 2015 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
@ -37,10 +37,6 @@ class ThreeMFReader(MeshReader):
super().__init__()
self._supported_extensions = [".3mf"]
self._root = None
self._namespaces = {
"3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
"cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
}
self._base_name = ""
self._unit = None
self._object_count = 0 # Used to name objects as there is no node name yet.
@ -81,8 +77,10 @@ class ThreeMFReader(MeshReader):
self._object_count += 1
node_name = "Object %s" % self._object_count
active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
um_node = CuraSceneNode()
um_node.addDecorator(BuildPlateDecorator(0))
um_node.addDecorator(BuildPlateDecorator(active_build_plate))
um_node.setName(node_name)
transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
um_node.setTransformation(transformation)

View file

@ -24,16 +24,49 @@ from cura.Settings.ExtruderStack import ExtruderStack
from cura.Settings.GlobalStack import GlobalStack
from cura.Settings.CuraContainerStack import _ContainerIndexes
from cura.QualityManager import QualityManager
from cura.CuraApplication import CuraApplication
from configparser import ConfigParser
import zipfile
import io
import configparser
import os
import threading
i18n_catalog = i18nCatalog("cura")
#
# HACK:
#
# In project loading, when override the existing machine is selected, the stacks and containers that are correctly
# active in the system will be overridden at runtime. Because the project loading is done in a different thread than
# the Qt thread, something else can kick in the middle of the process. One of them is the rendering. It will access
# the current stacks and container, which have not completely been updated yet, so Cura will crash in this case.
#
# This "@call_on_qt_thread" decorator makes sure that a function will always be called on the Qt thread (blocking).
# It is applied to the read() function of project loading so it can be guaranteed that only after the project loading
# process is completely done, everything else that needs to occupy the QT thread will be executed.
#
class InterCallObject:
def __init__(self):
self.finish_event = threading.Event()
self.result = None
def call_on_qt_thread(func):
def _call_on_qt_thread_wrapper(*args, **kwargs):
def _handle_call(ico, *args, **kwargs):
ico.result = func(*args, **kwargs)
ico.finish_event.set()
inter_call_object = InterCallObject()
new_args = tuple([inter_call_object] + list(args)[:])
CuraApplication.getInstance().callLater(_handle_call, *new_args, **kwargs)
inter_call_object.finish_event.wait()
return inter_call_object.result
return _call_on_qt_thread_wrapper
## Base implementation for reading 3MF workspace files.
class ThreeMFWorkspaceReader(WorkspaceReader):
def __init__(self):
@ -89,7 +122,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# The default ContainerStack.deserialize() will connect signals, which is not desired in this case.
# Since we know that the stack files are INI files, so we directly use the ConfigParser to parse them.
serialized = archive.open(file_name).read().decode("utf-8")
stack_config = ConfigParser()
stack_config = ConfigParser(interpolation = None)
stack_config.read_string(serialized)
# sanity check
@ -168,11 +201,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
Logger.log("w", "Unknown definition container type %s for %s",
definition_container_type, each_definition_container_file)
Job.yieldThread()
# sanity check
if machine_definition_container_count != 1:
msg = "Expecting one machine definition container but got %s" % machine_definition_container_count
Logger.log("e", msg)
raise RuntimeError(msg)
return WorkspaceReader.PreReadResult.failed #Not a workspace file but ordinary 3MF.
material_labels = []
material_conflict = False
@ -271,9 +302,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# if the global stack is found, we check if there are conflicts in the extruder stacks
if containers_found_dict["machine"] and not machine_conflict:
for extruder_stack_file in extruder_stack_files:
container_id = self._stripFileToId(extruder_stack_file)
serialized = archive.open(extruder_stack_file).read().decode("utf-8")
parser = configparser.ConfigParser()
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
# The check should be done for the extruder stack that's associated with the existing global stack,
@ -303,7 +333,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
break
num_visible_settings = 0
has_visible_settings_string = False
try:
temp_preferences = Preferences()
serialized = archive.open("Cura/preferences.cfg").read().decode("utf-8")
@ -378,7 +407,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
## Overrides an ExtruderStack in the given GlobalStack and returns the new ExtruderStack.
def _overrideExtruderStack(self, global_stack, extruder_file_content, extruder_stack_file):
# Get extruder position first
extruder_config = configparser.ConfigParser()
extruder_config = configparser.ConfigParser(interpolation = None)
extruder_config.read_string(extruder_file_content)
if not extruder_config.has_option("metadata", "position"):
msg = "Could not find 'metadata/position' in extruder stack file"
@ -404,6 +433,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# containing global.cfg / extruder.cfg
#
# \param file_name
@call_on_qt_thread
def read(self, file_name):
archive = zipfile.ZipFile(file_name, "r")
@ -423,6 +453,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
Logger.log("w", "Workspace did not contain visible settings. Leaving visibility unchanged")
else:
global_preferences.setValue("general/visible_settings", visible_settings)
global_preferences.setValue("general/preset_setting_visibility_choice", "Custom")
categories_expanded = temp_preferences.getValue("cura/categories_expanded")
if categories_expanded is None:
@ -528,12 +559,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)]
user_instance_containers = []
quality_and_definition_changes_instance_containers = []
quality_changes_instance_containers = []
for instance_container_file in instance_container_files:
container_id = self._stripFileToId(instance_container_file)
serialized = archive.open(instance_container_file).read().decode("utf-8")
# HACK! we ignore "quality" and "variant" instance containers!
parser = configparser.ConfigParser()
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
if not parser.has_option("metadata", "type"):
Logger.log("w", "Cannot find metadata/type in %s, ignoring it", instance_container_file)
@ -609,7 +641,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
instance_container.setName(self._container_registry.uniqueName(instance_container.getName()))
new_changes_container_id = self.getNewId(instance_container.getId())
instance_container._id = new_changes_container_id
instance_container.setMetaDataEntry("id", new_changes_container_id)
# TODO: we don't know the following is correct or not, need to verify
# AND REFACTOR!!!
@ -633,6 +665,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# The ID already exists, but nothing in the values changed, so do nothing.
pass
quality_and_definition_changes_instance_containers.append(instance_container)
if container_type == "quality_changes":
quality_changes_instance_containers.append(instance_container)
if container_type == "definition_changes":
definition_changes_extruder_count = instance_container.getProperty("machine_extruder_count", "value")
@ -688,7 +722,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
stack.setName(global_stack_name_new)
container_stacks_added.append(stack)
self._container_registry.addContainer(stack)
# self._container_registry.addContainer(stack)
containers_added.append(stack)
else:
Logger.log("e", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"])
@ -706,6 +740,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
return
# load extruder stack files
has_extruder_stack_files = len(extruder_stack_files) > 0
empty_quality_container = self._container_registry.findInstanceContainers(id = "empty_quality")[0]
empty_quality_changes_container = self._container_registry.findInstanceContainers(id = "empty_quality_changes")[0]
try:
for extruder_stack_file in extruder_stack_files:
container_id = self._stripFileToId(extruder_stack_file)
@ -726,7 +763,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# deserialize() by setting the metadata, but in the case of ExtruderStack, deserialize()
# also does addExtruder() to its machine stack, so we have to make sure that it's pointing
# to the right machine BEFORE deserialization.
extruder_config = configparser.ConfigParser()
extruder_config = configparser.ConfigParser(interpolation = None)
extruder_config.read_string(extruder_file_content)
extruder_config.set("metadata", "machine", global_stack_id_new)
tmp_string_io = io.StringIO()
@ -754,8 +791,24 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# If not extruder stacks were saved in the project file (pre 3.1) create one manually
# We re-use the container registry's addExtruderStackForSingleExtrusionMachine method for this
if not extruder_stacks:
stack = self._container_registry.addExtruderStackForSingleExtrusionMachine(global_stack, "fdmextruder")
if stack:
# If we choose to override a machine but to create a new custom quality profile, the custom quality
# profile is not immediately applied to the global_stack, so this fix for single extrusion machines
# will use the current custom quality profile on the existing machine. The extra optional argument
# in that function is used in this case to specify a new global stack quality_changes container so
# the fix can correctly create and copy over the custom quality settings to the newly created extruder.
new_global_quality_changes = None
if self._resolve_strategies["quality_changes"] == "new" and len(quality_changes_instance_containers) > 0:
new_global_quality_changes = quality_changes_instance_containers[0]
# Depending if the strategy is to create a new or override, the ids must be or not be unique
stack = self._container_registry.addExtruderStackForSingleExtrusionMachine(global_stack, "fdmextruder",
new_global_quality_changes,
create_new_ids = self._resolve_strategies["machine"] == "new")
if new_global_quality_changes is not None:
quality_changes_instance_containers.append(stack.qualityChanges)
quality_and_definition_changes_instance_containers.append(stack.qualityChanges)
if global_stack.quality.getId() in ("empty", "empty_quality"):
stack.quality = empty_quality_container
if self._resolve_strategies["machine"] == "override":
# in case the extruder is newly created (for a single-extrusion machine), we need to override
# the existing extruder stack.
@ -773,6 +826,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._container_registry.removeContainer(container.getId())
return
## In case there is a new machine and once the extruders are created, the global stack is added to the registry,
# otherwise the accContainers function in CuraContainerRegistry will create an extruder stack and then creating
# useless files
if self._resolve_strategies["machine"] == "new":
self._container_registry.addContainer(global_stack)
# Check quality profiles to make sure that if one stack has the "not supported" quality profile,
# all others should have the same.
@ -805,17 +863,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
quality_has_been_changed = False
if has_not_supported:
empty_quality_container = self._container_registry.findInstanceContainers(id = "empty_quality")[0]
for stack in [global_stack] + extruder_stacks_in_use:
stack.replaceContainer(_ContainerIndexes.Quality, empty_quality_container)
empty_quality_changes_container = self._container_registry.findInstanceContainers(id = "empty_quality_changes")[0]
for stack in [global_stack] + extruder_stacks_in_use:
stack.replaceContainer(_ContainerIndexes.QualityChanges, empty_quality_changes_container)
quality_has_been_changed = True
else:
empty_quality_changes_container = self._container_registry.findInstanceContainers(id="empty_quality_changes")[0]
# The machine in the project has non-empty quality and there are usable qualities for this machine.
# We need to check if the current quality_type is still usable for this machine, if not, then the quality
# will be reset to the "preferred quality" if present, otherwise "normal".
@ -926,9 +979,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# sanity checks
# NOTE: The following cases SHOULD NOT happen!!!!
if not old_container:
if old_container.getId() in ("empty_quality_changes", "empty_definition_changes", "empty"):
Logger.log("e", "We try to get [%s] from the global stack [%s] but we got None instead!",
changes_container_type, global_stack.getId())
continue
# Replace the quality/definition changes container if it's in the GlobalStack
# NOTE: we can get an empty container here, but the IDs will not match,
@ -941,6 +995,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
continue
# Replace the quality/definition changes container if it's in one of the ExtruderStacks
# Only apply the change if we have loaded extruder stacks from the project
if has_extruder_stack_files:
for each_extruder_stack in extruder_stacks:
changes_container = None
if changes_container_type == "quality_changes":
@ -950,9 +1006,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# sanity checks
# NOTE: The following cases SHOULD NOT happen!!!!
if not changes_container:
if changes_container.getId() in ("empty_quality_changes", "empty_definition_changes", "empty"):
Logger.log("e", "We try to get [%s] from the extruder stack [%s] but we got None instead!",
changes_container_type, each_extruder_stack.getId())
continue
# NOTE: we can get an empty container here, but the IDs will not match,
# so this comparison is fine.
@ -995,6 +1052,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
for stack in extruder_stacks:
stack.setNextStack(global_stack)
stack.containersChanged.emit(stack.getTop())
else:
CuraApplication.getInstance().getMachineManager().activeQualityChanged.emit()
# Actually change the active machine.
Application.getInstance().setGlobalContainerStack(global_stack)
@ -1044,13 +1103,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# find the old material ID
old_material_id_in_stack = stack.material.getId()
best_matching_old_material_id = None
best_matching_old_meterial_prefix_length = -1
best_matching_old_material_prefix_length = -1
for old_parent_material_id in old_new_material_dict:
if len(old_parent_material_id) < best_matching_old_meterial_prefix_length:
if len(old_parent_material_id) < best_matching_old_material_prefix_length:
continue
if len(old_parent_material_id) <= len(old_material_id_in_stack):
if old_parent_material_id == old_material_id_in_stack[0:len(old_parent_material_id)]:
best_matching_old_meterial_prefix_length = len(old_parent_material_id)
best_matching_old_material_prefix_length = len(old_parent_material_id)
best_matching_old_material_id = old_parent_material_id
if best_matching_old_material_id is None:

View file

@ -390,6 +390,13 @@ UM.Dialog
}
}
function accept() {
manager.closeBackend();
manager.onOkButtonClicked();
base.visible = false;
base.accept();
}
function reject() {
manager.onCancelButtonClicked();
base.visible = false;

View file

@ -57,7 +57,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
# Save Cura version
version_file = zipfile.ZipInfo("Cura/version.ini")
version_config_parser = configparser.ConfigParser()
version_config_parser = configparser.ConfigParser(interpolation = None)
version_config_parser.add_section("versions")
version_config_parser.set("versions", "cura_version", Application.getInstance().getVersion())
version_config_parser.set("versions", "build_type", Application.getInstance().getBuildType())
@ -97,7 +97,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
file_in_archive.compress_type = zipfile.ZIP_DEFLATED
# Do not include the network authentication keys
ignore_keys = {"network_authentication_id", "network_authentication_key"}
ignore_keys = {"network_authentication_id", "network_authentication_key", "octoprint_api_key"}
serialized_data = container.serialize(ignored_metadata_keys = ignore_keys)
archive.writestr(file_in_archive, serialized_data)

View file

@ -6,8 +6,9 @@ from UM.Math.Vector import Vector
from UM.Logger import Logger
from UM.Math.Matrix import Matrix
from UM.Application import Application
import UM.Scene.SceneNode
from cura.Scene.CuraSceneNode import CuraSceneNode
from UM.Scene.SceneNode import SceneNode
from cura.CuraApplication import CuraApplication
import Savitar
@ -62,11 +63,15 @@ class ThreeMFWriter(MeshWriter):
self._store_archive = store_archive
## Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
# \returns Uranium Scenen node.
# \returns Uranium Scene node.
def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()):
if type(um_node) not in [UM.Scene.SceneNode.SceneNode, CuraSceneNode]:
if not isinstance(um_node, SceneNode):
return None
active_build_plate_nr = CuraApplication.getInstance().getBuildPlateModel().activeBuildPlate
if um_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
return
savitar_node = Savitar.SceneNode()
node_matrix = um_node.getLocalTransformation()
@ -97,6 +102,9 @@ class ThreeMFWriter(MeshWriter):
savitar_node.setSetting(key, str(stack.getProperty(key, "value")))
for child_node in um_node.getChildren():
# only save the nodes on the active build plate
if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
continue
savitar_child_node = self._convertUMNodeToSavitarNode(child_node)
if savitar_child_node is not None:
savitar_node.addChild(savitar_child_node)

View file

@ -9,6 +9,7 @@ from UM.Application import Application
from UM.Resources import Resources
from UM.Logger import Logger
class AutoSave(Extension):
def __init__(self):
super().__init__()
@ -16,18 +17,40 @@ class AutoSave(Extension):
Preferences.getInstance().preferenceChanged.connect(self._triggerTimer)
self._global_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
Preferences.getInstance().addPreference("cura/autosave_delay", 1000 * 10)
self._change_timer = QTimer()
self._change_timer.setInterval(Preferences.getInstance().getValue("cura/autosave_delay"))
self._change_timer.setSingleShot(True)
self._change_timer.timeout.connect(self._onTimeout)
self._saving = False
# At this point, the Application instance has not finished its constructor call yet, so directly using something
# like Application.getInstance() is not correct. The initialisation now will only gets triggered after the
# application finishes its start up successfully.
self._init_timer = QTimer()
self._init_timer.setInterval(1000)
self._init_timer.setSingleShot(True)
self._init_timer.timeout.connect(self.initialize)
self._init_timer.start()
def initialize(self):
# only initialise if the application is created and has started
from cura.CuraApplication import CuraApplication
if not CuraApplication.Created:
self._init_timer.start()
return
if not CuraApplication.getInstance().started:
self._init_timer.start()
return
self._change_timer.timeout.connect(self._onTimeout)
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
self._triggerTimer()
def _triggerTimer(self, *args):
if not self._saving:
self._change_timer.start()

View file

@ -1,3 +1,49 @@
[3.2.0]
*Tree support
Experimental tree-like support structure that uses branches to support prints. Branches grow and multiply towards the model, with fewer contact points than alternative support methods. This results in better surface finishes for organic-shaped prints.
*Adaptive layers
Prints with a variable layer thickness which adapts to the angle of the models surfaces. The result is high-quality surface finishes with a marginally increased print time. This setting can be found under the experimental category.
*Faster startup
Printer definitions are now loaded when adding a printer, instead of loading all available printers on startup.
*Backface culling in layer view
Doubled frame rate by only rendering visible surfaces of the model in the layer view, instead of rendering the entire model. Good for lower spec GPUs as it is less resource-intensive.
*Multi build plate
Experimental feature that creates separate build plates with shared settings in a single session, eliminating the need to clear the build plate multiple times. Multiple build plates can be sliced and sent to a printer or printer group in Cura Connect. This feature must be enabled manually in the preferences general tab.
*Improved mesh type selection
New button in the left toolbar to edit per model settings, giving the user more control over where to place support. Objects can be used as meshes, with a drop down list where Print as support, Don't overlap support with other models, Modify settings for overlap with other models, or Modify settings for infill of other models can be specified. Contributed by fieldOfView.
*View optimization
Quick camera controls introduced in version 3.1 have been revised to create more accurate isometric, front, left, and right views.
*Updated sidebar to QtQuick 2.0
Application framework updated to increase speed, achieve a better width and style fit, and gives users dropdown menus that are styled to fit the enabled Ultimaker Cura theme, instead of the operating systems theme.
*Hide sidebar
The sidebar can now be hidden/shown by selecting View > Expand/Collapse Sidebar, or with the hotkey CMD + E (Mac) or CTRL + E (PC and Linux).
*Disable Send slice information
A shortcut to disable Send slice information has been added to the first launch to make it easier for privacy-conscious users to keep slice information private.
*Signed binaries (Windows)
For security-conscious users, the Windows installer and Windows binaries have been digitally signed to prevent “Unknown application” warnings and virus scanner false-positives.
*Start/end gcode script per extruder
Variables from both extruders in the start and end gcode snippets can now be accessed and edited, creating uniformity between profiles in different slicing environments. Contributed by fieldOfView.
*OctoPrint plugin added to plugin browser
This plugin enables printers managed with OctoPrint to print via Ultimaker Cura interface (version 3.2 or later).
*Bugfixes
- Fixed a bug where the mirror tool and center model options when used together would reset the model transformations
- Updated config file path to fix crashes caused by user config files that are located on remote drives
- Updated Arduino drivers to fix triggering errors during OTA updates in shared environments. This also fixes an issue when upgrading the firmware of the Ultimaker Original.
- Fixed an issue where arranging small models would fail, due to conflict with small model files combined with the “Ensure models are kept apart” option
[3.1.0]
*Profile added for 0.25 mm print core
This new print core gives extra fine line widths which gives prints extra definition and surface quality.

View file

@ -88,7 +88,7 @@ class CuraEngineBackend(QObject, Backend):
#
self._global_container_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
Application.getInstance().getExtruderManager().activeExtruderChanged.connect(self._onGlobalStackChanged)
Application.getInstance().getExtruderManager().extrudersAdded.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished)
@ -187,9 +187,7 @@ class CuraEngineBackend(QObject, Backend):
## Manually triggers a reslice
@pyqtSlot()
def forceSlice(self):
if self._use_timer:
self._change_timer.start()
else:
self.markSliceAll()
self.slice()
## Perform a slice of the scene.
@ -198,23 +196,26 @@ class CuraEngineBackend(QObject, Backend):
self._slice_start_time = time()
if not self._build_plates_to_be_sliced:
self.processingProgress.emit(1.0)
self.backendStateChange.emit(BackendState.Done)
Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.")
return
if self._process_layers_job:
Logger.log("d", " ## Process layers job still busy, trying later")
self._invokeSlice()
return
if not hasattr(self._scene, "gcode_dict"):
self._scene.gcode_dict = {}
# see if we really have to slice
active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0)
Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced)
num_objects = self._numObjects()
if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0:
Logger.log("d", "Build plate %s has 0 objects to be sliced, skipping", build_plate_to_be_sliced)
self._invokeSlice()
self._scene.gcode_dict[build_plate_to_be_sliced] = []
Logger.log("d", "Build plate %s has no objects to be sliced, skipping", build_plate_to_be_sliced)
if self._build_plates_to_be_sliced:
self.slice()
return
self._stored_layer_data = []
@ -231,12 +232,12 @@ class CuraEngineBackend(QObject, Backend):
self.processingProgress.emit(0.0)
self.backendStateChange.emit(BackendState.NotStarted)
if not hasattr(self._scene, "gcode_list"):
self._scene.gcode_list = {}
self._scene.gcode_list[build_plate_to_be_sliced] = [] #[] indexed by build plate number
self._scene.gcode_dict[build_plate_to_be_sliced] = [] #[] indexed by build plate number
self._slicing = True
self.slicingStarted.emit()
self.determineAutoSlicing() # Switch timer on or off if appropriate
slice_message = self._socket.createMessage("cura.proto.Slice")
self._start_slice_job = StartSliceJob.StartSliceJob(slice_message)
self._start_slice_job_build_plate = build_plate_to_be_sliced
@ -288,6 +289,7 @@ class CuraEngineBackend(QObject, Backend):
self._start_slice_job = None
if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error:
self.backendStateChange.emit(BackendState.Error)
return
if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible:
@ -357,6 +359,17 @@ class CuraEngineBackend(QObject, Backend):
else:
self.backendStateChange.emit(BackendState.NotStarted)
if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice:
if Application.getInstance().platformActivity:
self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."),
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
self.backendStateChange.emit(BackendState.Error)
else:
self.backendStateChange.emit(BackendState.NotStarted)
self._invokeSlice()
return
# Preparation completed, send it to the backend.
self._socket.sendMessage(job.getSliceMessage())
@ -380,7 +393,7 @@ class CuraEngineBackend(QObject, Backend):
self.backendStateChange.emit(BackendState.Disabled)
gcode_list = node.callDecoration("getGCodeList")
if gcode_list is not None:
self._scene.gcode_list[node.callDecoration("getBuildPlateNumber")] = gcode_list
self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list
if self._use_timer == enable_timer:
return self._use_timer
@ -408,9 +421,14 @@ class CuraEngineBackend(QObject, Backend):
#
# \param source The scene node that was changed.
def _onSceneChanged(self, source):
if not issubclass(type(source), SceneNode):
if not isinstance(source, SceneNode):
return
# This case checks if the source node is a node that contains GCode. In this case the
# current layer data is removed so the previous data is not rendered - CURA-4821
if source.callDecoration("isBlockSlicing") and source.callDecoration("getLayerData"):
self._stored_optimized_layer_data = {}
build_plate_changed = set()
source_build_plate_number = source.callDecoration("getBuildPlateNumber")
if source == self._scene.getRoot():
@ -445,6 +463,7 @@ class CuraEngineBackend(QObject, Backend):
for build_plate_number in build_plate_changed:
if build_plate_number not in self._build_plates_to_be_sliced:
self._build_plates_to_be_sliced.append(build_plate_number)
self.printDurationMessage.emit(source_build_plate_number, {}, [])
self.processingProgress.emit(0.0)
self.backendStateChange.emit(BackendState.NotStarted)
# if not self._use_timer:
@ -507,7 +526,7 @@ class CuraEngineBackend(QObject, Backend):
def _onStackErrorCheckFinished(self):
self._is_error_check_scheduled = False
if not self._slicing and self._build_plates_to_be_sliced: #self._need_slicing:
if not self._slicing and self._build_plates_to_be_sliced:
self.needsSlicing()
self._onChanged()
@ -521,6 +540,8 @@ class CuraEngineBackend(QObject, Backend):
#
# \param message The protobuf message containing sliced layer data.
def _onOptimizedLayerMessage(self, message):
if self._start_slice_job_build_plate not in self._stored_optimized_layer_data:
self._stored_optimized_layer_data[self._start_slice_job_build_plate] = []
self._stored_optimized_layer_data[self._start_slice_job_build_plate].append(message)
## Called when a progress message is received from the engine.
@ -547,7 +568,7 @@ class CuraEngineBackend(QObject, Backend):
self.backendStateChange.emit(BackendState.Done)
self.processingProgress.emit(1.0)
gcode_list = self._scene.gcode_list[self._start_slice_job_build_plate]
gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate]
for index, line in enumerate(gcode_list):
replaced = line.replace("{print_time}", str(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601)))
replaced = replaced.replace("{filament_amount}", str(Application.getInstance().getPrintInformation().materialLengths))
@ -570,21 +591,23 @@ class CuraEngineBackend(QObject, Backend):
Logger.log("d", "See if there is more to slice...")
# Somehow this results in an Arcus Error
# self.slice()
# Testing call slice again, allow backend to restart by using the timer
# Call slice again using the timer, allowing the backend to restart
if self._build_plates_to_be_sliced:
self.enableTimer() # manually enable timer to be able to invoke slice, also when in manual slice mode
self._invokeSlice()
## Called when a g-code message is received from the engine.
#
# \param message The protobuf message containing g-code, encoded as UTF-8.
def _onGCodeLayerMessage(self, message):
self._scene.gcode_list[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace"))
self._scene.gcode_dict[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace"))
## Called when a g-code prefix message is received from the engine.
#
# \param message The protobuf message containing the g-code prefix,
# encoded as UTF-8.
def _onGCodePrefixMessage(self, message):
self._scene.gcode_list[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace"))
self._scene.gcode_dict[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace"))
## Creates a new socket connection.
def _createSocket(self):
@ -704,7 +727,7 @@ class CuraEngineBackend(QObject, Backend):
if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged)
self._global_container_stack.containersChanged.disconnect(self._onChanged)
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
extruders = list(self._global_container_stack.extruders.values())
for extruder in extruders:
extruder.propertyChanged.disconnect(self._onSettingChanged)
@ -715,7 +738,7 @@ class CuraEngineBackend(QObject, Backend):
if self._global_container_stack:
self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed.
self._global_container_stack.containersChanged.connect(self._onChanged)
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
extruders = list(self._global_container_stack.extruders.values())
for extruder in extruders:
extruder.propertyChanged.connect(self._onSettingChanged)
extruder.containersChanged.connect(self._onChanged)

View file

@ -12,4 +12,6 @@ class ProcessGCodeLayerJob(Job):
self._message = message
def run(self):
self._scene.gcode_list.append(self._message.data.decode("utf-8", "replace"))
active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate
gcode_list = self._scene.gcode_dict[active_build_plate_id]
gcode_list.append(self._message.data.decode("utf-8", "replace"))

View file

@ -4,7 +4,6 @@
import gc
from UM.Job import Job
from UM.Scene.SceneNode import SceneNode
from UM.Application import Application
from UM.Mesh.MeshData import MeshData
from UM.Preferences import Preferences
@ -17,6 +16,7 @@ from UM.Logger import Logger
from UM.Math.Vector import Vector
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Settings.ExtruderManager import ExtruderManager
from cura import LayerDataBuilder
from cura import LayerDataDecorator
@ -81,7 +81,7 @@ class ProcessSlicedLayersJob(Job):
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
new_node = SceneNode()
new_node = CuraSceneNode()
new_node.addDecorator(BuildPlateDecorator(self._build_plate_number))
# Force garbage collection.

View file

@ -5,6 +5,7 @@ import numpy
from string import Formatter
from enum import IntEnum
import time
import re
from UM.Job import Job
from UM.Application import Application
@ -15,7 +16,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Settings.Validator import ValidatorState
from UM.Settings.SettingRelation import RelationType
from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.OneAtATimeIterator import OneAtATimeIterator
from cura.Settings.ExtruderManager import ExtruderManager
@ -122,6 +123,12 @@ class StartSliceJob(Job):
self.setResult(StartJobResult.BuildPlateError)
return
# Don't slice if the buildplate or the nozzle type is incompatible with the materials
if not Application.getInstance().getMachineManager().variantBuildplateCompatible and \
not Application.getInstance().getMachineManager().variantBuildplateUsable:
self.setResult(StartJobResult.MaterialIncompatible)
return
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
material = extruder_stack.findContainer({"type": "material"})
if material:
@ -131,7 +138,7 @@ class StartSliceJob(Job):
# Don't slice if there is a per object setting with an error value.
for node in DepthFirstIterator(self._scene.getRoot()):
if type(node) is not SceneNode or not node.isSelectable():
if node.isSelectable():
continue
if self._checkStackForErrors(node.callDecoration("getStack")):
@ -155,10 +162,15 @@ class StartSliceJob(Job):
if getattr(node, "_outside_buildarea", False):
continue
# Filter on current build plate
build_plate_number = node.callDecoration("getBuildPlateNumber")
if build_plate_number is not None and build_plate_number != self._build_plate_number:
continue
children = node.getAllChildren()
children.append(node)
for child_node in children:
if type(child_node) is SceneNode and child_node.getMeshData() and child_node.getMeshData().getVertices() is not None:
if child_node.getMeshData() and child_node.getMeshData().getVertices() is not None:
temp_list.append(child_node)
if temp_list:
@ -170,13 +182,13 @@ class StartSliceJob(Job):
temp_list = []
has_printing_mesh = False
for node in DepthFirstIterator(self._scene.getRoot()):
if node.callDecoration("isSliceable") and type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
if node.callDecoration("isSliceable") and node.getMeshData() and node.getMeshData().getVertices() is not None:
per_object_stack = node.callDecoration("getStack")
is_non_printing_mesh = False
if per_object_stack:
is_non_printing_mesh = any(per_object_stack.getProperty(key, "value") for key in NON_PRINTING_MESH_SETTINGS)
if (node.callDecoration("getBuildPlateNumber") == self._build_plate_number):
if node.callDecoration("getBuildPlateNumber") == self._build_plate_number:
if not getattr(node, "_outside_buildarea", False) or is_non_printing_mesh:
temp_list.append(node)
if not is_non_printing_mesh:
@ -332,10 +344,12 @@ class StartSliceJob(Job):
# Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
start_gcode = settings["machine_start_gcode"]
bed_temperature_settings = {"material_bed_temperature", "material_bed_temperature_layer_0"}
settings["material_bed_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in bed_temperature_settings))
print_temperature_settings = {"material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"}
settings["material_print_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in print_temperature_settings))
bed_temperature_settings = ["material_bed_temperature", "material_bed_temperature_layer_0"]
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(bed_temperature_settings) # match {setting} as well as {setting, extruder_nr}
settings["material_bed_temp_prepend"] = re.search(pattern, start_gcode) == None
print_temperature_settings = ["material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"]
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(print_temperature_settings) # match {setting} as well as {setting, extruder_nr}
settings["material_print_temp_prepend"] = re.search(pattern, start_gcode) == None
# Replace the setting tokens in start and end g-code.
# Use values from the first used extruder by default so we get the expected temperatures

View file

@ -52,10 +52,10 @@ class CuraProfileReader(ProfileReader):
parser = configparser.ConfigParser(interpolation=None)
parser.read_string(serialized)
if not "general" in parser:
if "general" not in parser:
Logger.log("w", "Missing required section 'general'.")
return []
if not "version" in parser["general"]:
if "version" not in parser["general"]:
Logger.log("w", "Missing required 'version' property")
return []

View file

@ -49,7 +49,6 @@ class FirmwareUpdateChecker(Extension):
def _onContainerAdded(self, container):
# Only take care when a new GlobalStack was added
if isinstance(container, GlobalStack):
Logger.log("i", "You have a '%s' in printer list. Let's check the firmware!", container.getId())
self.checkFirmwareVersion(container, True)
## Connect with software.ultimaker.com, load latest.version and check version info.

View file

@ -44,6 +44,8 @@ class FirmwareUpdateCheckerJob(Job):
# Now we just do that if the active printer is Ultimaker 3 or Ultimaker 3 Extended or any
# other Ultimaker 3 that will come in the future
if len(machine_name_parts) >= 2 and machine_name_parts[:2] == ["ultimaker", "3"]:
Logger.log("i", "You have a UM3 in printer list. Let's check the firmware!")
# Nothing to parse, just get the string
# TODO: In the future may be done by parsing a JSON file with diferent version for each printer model
current_version = reader(current_version_file).readline().rstrip()

View file

@ -8,14 +8,14 @@ from UM.Logger import Logger
from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Vector import Vector
from UM.Message import Message
from UM.Scene.SceneNode import SceneNode
from cura.Scene.CuraSceneNode import CuraSceneNode
from UM.i18n import i18nCatalog
from UM.Preferences import Preferences
catalog = i18nCatalog("cura")
from cura import LayerDataBuilder
from cura import LayerDataDecorator
from cura.LayerDataDecorator import LayerDataDecorator
from cura.LayerPolygon import LayerPolygon
from cura.Scene.GCodeListDecorator import GCodeListDecorator
from cura.Settings.ExtruderManager import ExtruderManager
@ -292,7 +292,7 @@ class FlavorParser:
# We obtain the filament diameter from the selected printer to calculate line widths
self._filament_diameter = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value")
scene_node = SceneNode()
scene_node = CuraSceneNode()
# Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
# real data to calculate it from.
scene_node.getBoundingBox = self._getNullBoundingBox
@ -418,11 +418,17 @@ class FlavorParser:
self._layer_number += 1
current_path.clear()
material_color_map = numpy.zeros((10, 4), dtype = numpy.float32)
material_color_map = numpy.zeros((8, 4), dtype = numpy.float32)
material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0]
material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0]
material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0]
material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0]
material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0]
material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0]
layer_mesh = self._layer_data_builder.build(material_color_map)
decorator = LayerDataDecorator.LayerDataDecorator()
decorator = LayerDataDecorator()
decorator.setLayerData(layer_mesh)
scene_node.addDecorator(decorator)
@ -430,7 +436,10 @@ class FlavorParser:
gcode_list_decorator.setGCodeList(gcode_list)
scene_node.addDecorator(gcode_list_decorator)
Application.getInstance().getController().getScene().gcode_list = gcode_list
# gcode_dict stores gcode_lists for a number of build plates.
active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate
gcode_dict = {active_build_plate_id: gcode_list}
Application.getInstance().getController().getScene().gcode_dict = gcode_dict
Logger.log("d", "Finished parsing %s" % file_name)
self._message.hide()

View file

@ -26,7 +26,7 @@ class GCodeReader(MeshReader):
# PreRead is used to get the correct flavor. If not, Marlin is set by default
def preRead(self, file_name, *args, **kwargs):
with open(file_name, "r") as file:
with open(file_name, "r", encoding = "utf-8") as file:
for line in file:
if line[:len(self._flavor_keyword)] == self._flavor_keyword:
try:

View file

@ -61,8 +61,11 @@ class GCodeWriter(MeshWriter):
active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
scene = Application.getInstance().getController().getScene()
gcode_list = getattr(scene, "gcode_list")[active_build_plate]
if gcode_list:
gcode_dict = getattr(scene, "gcode_dict")
if not gcode_dict:
return False
gcode_list = gcode_dict.get(active_build_plate, None)
if gcode_list is not None:
for gcode in gcode_list:
stream.write(gcode)
# Serialise the current container stack and put it at the end of the file.
@ -104,7 +107,7 @@ class GCodeWriter(MeshWriter):
prefix_length = len(prefix)
container_with_profile = stack.qualityChanges
if not container_with_profile:
if container_with_profile.getId() == "empty_quality_changes":
Logger.log("e", "No valid quality profile found, not writing settings to GCode!")
return ""
@ -120,9 +123,9 @@ class GCodeWriter(MeshWriter):
serialized = flat_global_container.serialize()
data = {"global_quality": serialized}
for extruder in sorted(ExtruderManager.getInstance().getMachineExtruders(stack.getId()), key = lambda k: k.getMetaDataEntry("position")):
for extruder in sorted(stack.extruders.values(), key = lambda k: k.getMetaDataEntry("position")):
extruder_quality = extruder.qualityChanges
if not extruder_quality:
if extruder_quality.getId() == "empty_quality_changes":
Logger.log("w", "No extruder quality profile found, not writing quality for extruder %s to file!", extruder.getId())
continue
flat_extruder_quality = self._createFlattenedContainerInstance(extruder.getTop(), extruder_quality)

View file

@ -125,7 +125,10 @@ class LegacyProfileReader(ProfileReader):
Logger.log("e", "Dictionary of Doom has no translation. Is it the correct JSON file?")
return None
current_printer_definition = global_container_stack.definition
profile.setDefinition(current_printer_definition.getId())
quality_definition = current_printer_definition.getMetaDataEntry("quality_definition")
if not quality_definition:
quality_definition = current_printer_definition.getId()
profile.setDefinition(quality_definition)
for new_setting in dict_of_doom["translation"]: # Evaluate all new settings that would get a value from the translations.
old_setting_expression = dict_of_doom["translation"][new_setting]
compiled = compile(old_setting_expression, new_setting, "eval")
@ -162,20 +165,21 @@ class LegacyProfileReader(ProfileReader):
data = stream.getvalue()
profile.deserialize(data)
# The definition can get reset to fdmprinter during the deserialization's upgrade. Here we set the definition
# again.
profile.setDefinition(quality_definition)
#We need to return one extruder stack and one global stack.
global_container_id = container_registry.uniqueName("Global Imported Legacy Profile")
global_profile = profile.duplicate(new_id = global_container_id, new_name = profile_id) #Needs to have the same name as the extruder profile.
global_profile.setDirty(True)
#Only the extruder stack has an extruder metadata entry.
profile.addMetaDataEntry("extruder", ExtruderManager.getInstance().getActiveExtruderStack().definition.getId())
profile_definition = "fdmprinter"
from UM.Util import parseBool
if parseBool(global_container_stack.getMetaDataEntry("has_machine_quality", "False")):
profile_definition = global_container_stack.getMetaDataEntry("quality_definition")
if not profile_definition:
profile_definition = global_container_stack.definition.getId()
global_profile.setDefinition(profile_definition)
#Split all settings into per-extruder and global settings.
for setting_key in profile.getAllKeys():
settable_per_extruder = global_container_stack.getProperty(setting_key, "settable_per_extruder")
if settable_per_extruder:
global_profile.removeInstance(setting_key)
else:
profile.removeInstance(setting_key)
return [global_profile, profile]
return [global_profile]

View file

@ -105,60 +105,9 @@ class MachineSettingsAction(MachineAction):
@pyqtSlot(int)
def setMachineExtruderCount(self, extruder_count):
extruder_manager = Application.getInstance().getExtruderManager()
definition_changes_container = self._global_container_stack.definitionChanges
if not self._global_container_stack or definition_changes_container == self._empty_container:
return
previous_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
if extruder_count == previous_extruder_count:
return
# reset all extruder number settings whose value is no longer valid
for setting_instance in self._global_container_stack.userChanges.findInstances():
setting_key = setting_instance.definition.key
if not self._global_container_stack.getProperty(setting_key, "type") in ("extruder", "optional_extruder"):
continue
old_value = int(self._global_container_stack.userChanges.getProperty(setting_key, "value"))
if old_value >= extruder_count:
self._global_container_stack.userChanges.removeInstance(setting_key)
Logger.log("d", "Reset [%s] because its old value [%s] is no longer valid ", setting_key, old_value)
# Check to see if any objects are set to print with an extruder that will no longer exist
root_node = Application.getInstance().getController().getScene().getRoot()
for node in DepthFirstIterator(root_node):
if node.getMeshData():
extruder_nr = node.callDecoration("getActiveExtruderPosition")
if extruder_nr is not None and int(extruder_nr) > extruder_count - 1:
node.callDecoration("setActiveExtruder", extruder_manager.getExtruderStack(extruder_count - 1).getId())
definition_changes_container.setProperty("machine_extruder_count", "value", extruder_count)
# Make sure one of the extruder stacks is active
extruder_manager.setActiveExtruderIndex(0)
# Move settable_per_extruder values out of the global container
# After CURA-4482 this should not be the case anymore, but we still want to support older project files.
global_user_container = self._global_container_stack.getTop()
if previous_extruder_count == 1:
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
global_user_container = self._global_container_stack.getTop()
for setting_instance in global_user_container.findInstances():
setting_key = setting_instance.definition.key
settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder")
if settable_per_extruder:
limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder"))
extruder_stack = extruder_stacks[max(0, limit_to_extruder)]
extruder_stack.getTop().setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value"))
global_user_container.removeInstance(setting_key)
self.forceUpdate()
# Note: this method was in this class before, but since it's quite generic and other plugins also need it
# it was moved to the machine manager instead. Now this method just calls the machine manager.
Application.getInstance().getMachineManager().setActiveMachineExtruderCount(extruder_count)
@pyqtSlot()
def forceUpdate(self):
@ -209,76 +158,4 @@ class MachineSettingsAction(MachineAction):
@pyqtSlot(int)
def updateMaterialForDiameter(self, extruder_position: int):
# Updates the material container to a material that matches the material diameter set for the printer
if not self._global_container_stack:
return
if not self._global_container_stack.getMetaDataEntry("has_materials", False):
return
extruder_stack = self._global_container_stack.extruders[str(extruder_position)]
material_diameter = extruder_stack.material.getProperty("material_diameter", "value")
if not material_diameter:
# in case of "empty" material
material_diameter = 0
material_approximate_diameter = str(round(material_diameter))
machine_diameter = extruder_stack.definitionChanges.getProperty("material_diameter", "value")
if not machine_diameter:
machine_diameter = extruder_stack.definition.getProperty("material_diameter", "value")
machine_approximate_diameter = str(round(machine_diameter))
if material_approximate_diameter != machine_approximate_diameter:
Logger.log("i", "The the currently active material(s) do not match the diameter set for the printer. Finding alternatives.")
if self._global_container_stack.getMetaDataEntry("has_machine_materials", False):
materials_definition = self._global_container_stack.definition.getId()
has_material_variants = self._global_container_stack.getMetaDataEntry("has_variants", False)
else:
materials_definition = "fdmprinter"
has_material_variants = False
old_material = extruder_stack.material
search_criteria = {
"type": "material",
"approximate_diameter": machine_approximate_diameter,
"material": old_material.getMetaDataEntry("material", "value"),
"brand": old_material.getMetaDataEntry("brand", "value"),
"supplier": old_material.getMetaDataEntry("supplier", "value"),
"color_name": old_material.getMetaDataEntry("color_name", "value"),
"definition": materials_definition
}
if has_material_variants:
search_criteria["variant"] = extruder_stack.variant.getId()
if old_material == self._empty_container:
search_criteria.pop("material", None)
search_criteria.pop("supplier", None)
search_criteria.pop("brand", None)
search_criteria.pop("definition", None)
search_criteria["id"] = extruder_stack.getMetaDataEntry("preferred_material")
materials = self._container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Same material with new diameter is not found, search for generic version of the same material type
search_criteria.pop("supplier", None)
search_criteria.pop("brand", None)
search_criteria["color_name"] = "Generic"
materials = self._container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Generic material with new diameter is not found, search for preferred material
search_criteria.pop("color_name", None)
search_criteria.pop("material", None)
search_criteria["id"] = extruder_stack.getMetaDataEntry("preferred_material")
materials = self._container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Preferred material with new diameter is not found, search for any material
search_criteria.pop("id", None)
materials = self._container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Just use empty material as a final fallback
materials = [self._empty_container]
Logger.log("i", "Selecting new material: %s", materials[0].getId())
extruder_stack.material = materials[0]
Application.getInstance().getExtruderManager().updateMaterialForDiameter(extruder_position)

View file

@ -390,7 +390,7 @@ Cura.MachineAction
visible: Cura.MachineManager.hasMaterials
sourceComponent: numericTextFieldWithUnit
property string settingKey: "material_diameter"
property string label: catalog.i18nc("@label", "Material diameter")
property string label: catalog.i18nc("@label", "Compatible material diameter")
property string unit: catalog.i18nc("@label", "mm")
property string tooltip: catalog.i18nc("@tooltip", "The nominal diameter of filament supported by the printer. The exact diameter will be overridden by the material and/or the profile.")
function afterOnEditingFinished()
@ -591,7 +591,7 @@ Cura.MachineAction
const value = propertyProvider.properties.value;
return value ? value : "";
}
validator: RegExpValidator { regExp: _allowNegative ? /-?[0-9\.]{0,6}/ : /[0-9\.]{0,6}/ }
validator: RegExpValidator { regExp: _allowNegative ? /-?[0-9\.,]{0,6}/ : /[0-9\.,]{0,6}/ }
onEditingFinished:
{
if (propertyProvider && text != propertyProvider.properties.value)
@ -826,10 +826,10 @@ Cura.MachineAction
printHeadPolygon[axis][side] = result;
return result;
}
validator: RegExpValidator { regExp: /[0-9\.]{0,6}/ }
validator: RegExpValidator { regExp: /[0-9\.,]{0,6}/ }
onEditingFinished:
{
printHeadPolygon[axis][side] = parseFloat(textField.text);
printHeadPolygon[axis][side] = parseFloat(textField.text.replace(',','.'));
var polygon = [];
polygon.push([-printHeadPolygon["x"]["min"], printHeadPolygon["y"]["max"]]);
polygon.push([-printHeadPolygon["x"]["min"],-printHeadPolygon["y"]["min"]]);

View file

@ -8,8 +8,9 @@ import Cura 1.0 as Cura
Item
{
width: parent.width
height: parent.height
// parent could be undefined as this component is not visible at all times
width: parent ? parent.width : 0
height: parent ? parent.height : 0
// We show a nice overlay on the 3D viewer when the current output device has no monitor view
Rectangle

View file

@ -14,60 +14,122 @@ class MonitorStage(CuraStage):
super().__init__(parent)
# Wait until QML engine is created, otherwise creating the new QML components will fail
Application.getInstance().engineCreatedSignal.connect(self._setComponents)
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
self._printer_output_device = None
# Update the status icon when the output device is changed
Application.getInstance().getOutputDeviceManager().activeDeviceChanged.connect(self._setIconSource)
self._active_print_job = None
self._active_printer = None
def _setComponents(self):
self._setMainOverlay()
self._setSidebar()
self._setIconSource()
def _setActivePrintJob(self, print_job):
if self._active_print_job != print_job:
if self._active_print_job:
self._active_print_job.stateChanged.disconnect(self._updateIconSource)
self._active_print_job = print_job
if self._active_print_job:
self._active_print_job.stateChanged.connect(self._updateIconSource)
def _setMainOverlay(self):
# Ensure that the right icon source is returned.
self._updateIconSource()
def _setActivePrinter(self, printer):
if self._active_printer != printer:
if self._active_printer:
self._active_printer.activePrintJobChanged.disconnect(self._onActivePrintJobChanged)
self._active_printer = printer
if self._active_printer:
self._setActivePrintJob(self._active_printer.activePrintJob)
# Jobs might change, so we need to listen to it's changes.
self._active_printer.activePrintJobChanged.connect(self._onActivePrintJobChanged)
else:
self._setActivePrintJob(None)
# Ensure that the right icon source is returned.
self._updateIconSource()
def _onActivePrintJobChanged(self):
self._setActivePrintJob(self._active_printer.activePrintJob)
def _onActivePrinterChanged(self):
self._setActivePrinter(self._printer_output_device.activePrinter)
def _onOutputDevicesChanged(self):
try:
# We assume that you are monitoring the device with the highest priority.
new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
if new_output_device != self._printer_output_device:
if self._printer_output_device:
self._printer_output_device.acceptsCommandsChanged.disconnect(self._updateIconSource)
self._printer_output_device.connectionStateChanged.disconnect(self._updateIconSource)
self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged)
self._printer_output_device = new_output_device
self._printer_output_device.acceptsCommandsChanged.connect(self._updateIconSource)
self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged)
self._printer_output_device.connectionStateChanged.connect(self._updateIconSource)
self._setActivePrinter(self._printer_output_device.activePrinter)
# Force an update of the icon source
self._updateIconSource()
except IndexError:
pass
def _onEngineCreated(self):
# We can only connect now, as we need to be sure that everything is loaded (plugins get created quite early)
Application.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
self._onOutputDevicesChanged()
self._updateMainOverlay()
self._updateSidebar()
self._updateIconSource()
def _updateMainOverlay(self):
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"), "MonitorMainView.qml")
self.addDisplayComponent("main", main_component_path)
def _setSidebar(self):
def _updateSidebar(self):
# TODO: currently the sidebar component for prepare and monitor stages is the same, this will change with the printer output device refactor!
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "Sidebar.qml")
self.addDisplayComponent("sidebar", sidebar_component_path)
def _setIconSource(self):
def _updateIconSource(self):
if Application.getInstance().getTheme() is not None:
icon_name = self._getActiveOutputDeviceStatusIcon()
self.setIconSource(Application.getInstance().getTheme().getIcon(icon_name))
## Find the correct status icon depending on the active output device state
def _getActiveOutputDeviceStatusIcon(self):
output_device = Application.getInstance().getOutputDeviceManager().getActiveDevice()
if not output_device:
# We assume that you are monitoring the device with the highest priority.
try:
output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
except IndexError:
return "tab_status_unknown"
if hasattr(output_device, "acceptsCommands") and not output_device.acceptsCommands:
if not output_device.acceptsCommands:
return "tab_status_unknown"
if not hasattr(output_device, "printerState") or not hasattr(output_device, "jobState"):
return "tab_status_unknown"
# TODO: refactor to use enum instead of hardcoded strings?
if output_device.printerState == "maintenance":
return "tab_status_busy"
if output_device.jobState in ["printing", "pre_print", "pausing", "resuming"]:
return "tab_status_busy"
if output_device.jobState == "wait_cleanup":
return "tab_status_finished"
if output_device.jobState in ["ready", ""]:
if output_device.activePrinter is None:
return "tab_status_connected"
if output_device.jobState == "paused":
# TODO: refactor to use enum instead of hardcoded strings?
if output_device.activePrinter.state == "maintenance":
return "tab_status_busy"
if output_device.activePrinter.activePrintJob is None:
return "tab_status_connected"
if output_device.activePrinter.activePrintJob.state in ["printing", "pre_print", "pausing", "resuming"]:
return "tab_status_busy"
if output_device.activePrinter.activePrintJob.state == "wait_cleanup":
return "tab_status_finished"
if output_device.activePrinter.activePrintJob.state in ["ready", ""]:
return "tab_status_connected"
if output_device.activePrinter.activePrintJob.state == "paused":
return "tab_status_paused"
if output_device.jobState == "error":
if output_device.activePrinter.activePrintJob.state == "error":
return "tab_status_stopped"
return "tab_status_unknown"

View file

@ -2,6 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
from UM.FlameProfiler import pyqtSlot
from UM.Application import Application
from UM.Settings.ContainerRegistry import ContainerRegistry
@ -22,6 +23,9 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
self._node = None
self._stack = None
# this is a set of settings that will be skipped if the user chooses to reset.
self._skip_reset_setting_set = set()
def setSelectedObjectId(self, id):
if id != self._selected_object_id:
self._selected_object_id = id
@ -36,6 +40,10 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
def selectedObjectId(self):
return self._selected_object_id
@pyqtSlot(str)
def addSkipResetSetting(self, setting_name):
self._skip_reset_setting_set.add(setting_name)
def setVisible(self, visible):
if not self._node:
return
@ -50,6 +58,9 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
# Remove all instances that are not in visibility list
for instance in all_instances:
# exceptionally skip setting
if instance.definition.key in self._skip_reset_setting_set:
continue
if instance.definition.key not in visible:
settings.removeInstance(instance.definition.key)
visibility_changed = True

View file

@ -18,6 +18,9 @@ Item {
width: childrenRect.width;
height: childrenRect.height;
property var all_categories_except_support: [ "machine_settings", "resolution", "shell", "infill", "material", "speed",
"travel", "cooling", "platform_adhesion", "dual", "meshfix", "blackmagic", "experimental"]
Column
{
id: items
@ -39,6 +42,13 @@ Item {
verticalAlignment: Text.AlignVCenter
}
UM.SettingPropertyProvider
{
id: meshTypePropertyProvider
containerStackId: Cura.MachineManager.activeMachineId
watchedProperties: [ "enabled" ]
}
ComboBox
{
id: meshTypeSelection
@ -49,36 +59,55 @@ Item {
model: ListModel
{
id: meshTypeModel
Component.onCompleted:
Component.onCompleted: meshTypeSelection.populateModel()
}
function populateModel()
{
meshTypeModel.append({
type: "",
text: catalog.i18nc("@label", "Normal model")
});
meshTypePropertyProvider.key = "support_mesh";
if(meshTypePropertyProvider.properties.enabled == "True")
{
meshTypeModel.append({
type: "support_mesh",
text: catalog.i18nc("@label", "Print as support")
});
}
meshTypePropertyProvider.key = "anti_overhang_mesh";
if(meshTypePropertyProvider.properties.enabled == "True")
{
meshTypeModel.append({
type: "anti_overhang_mesh",
text: catalog.i18nc("@label", "Don't support overlap with other models")
});
}
meshTypePropertyProvider.key = "cutting_mesh";
if(meshTypePropertyProvider.properties.enabled == "True")
{
meshTypeModel.append({
type: "cutting_mesh",
text: catalog.i18nc("@label", "Modify settings for overlap with other models")
});
}
meshTypePropertyProvider.key = "infill_mesh";
if(meshTypePropertyProvider.properties.enabled == "True")
{
meshTypeModel.append({
type: "infill_mesh",
text: catalog.i18nc("@label", "Modify settings for infill of other models")
});
}
meshTypeSelection.updateCurrentIndex();
}
}
function updateCurrentIndex()
{
var mesh_type = UM.ActiveTool.properties.getValue("MeshType");
meshTypeSelection.currentIndex = -1;
for(var index=0; index < meshTypeSelection.model.count; index++)
{
if(meshTypeSelection.model.get(index).type == mesh_type)
@ -91,6 +120,16 @@ Item {
}
}
Connections
{
target: Cura.MachineManager
onGlobalContainerChanged:
{
meshTypeSelection.model.clear();
meshTypeSelection.populateModel();
}
}
Connections
{
target: UM.Selection
@ -106,12 +145,12 @@ Item {
id: currentSettings
property int maximumHeight: 200 * screenScaleFactor
height: Math.min(contents.count * (UM.Theme.getSize("section").height + UM.Theme.getSize("default_lining").height), maximumHeight)
visible: ["support_mesh", "anti_overhang_mesh"].indexOf(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type) == -1
visible: meshTypeSelection.model.get(meshTypeSelection.currentIndex).type != "anti_overhang_mesh"
ScrollView
{
height: parent.height
width: UM.Theme.getSize("setting").width
width: UM.Theme.getSize("setting").width + UM.Theme.getSize("default_margin").width
style: UM.Theme.styles.scrollview
ListView
@ -124,7 +163,15 @@ Item {
id: addedSettingsModel;
containerId: Cura.MachineManager.activeDefinitionId
expanded: [ "*" ]
exclude: [ "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ]
exclude: {
var excluded_settings = [ "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ];
if(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type == "support_mesh")
{
excluded_settings = excluded_settings.concat(base.all_categories_except_support);
}
return excluded_settings;
}
visibilityHandler: Cura.PerObjectSettingVisibilityHandler
{
@ -306,7 +353,18 @@ Item {
}
}
onClicked: settingPickDialog.visible = true;
onClicked:
{
settingPickDialog.visible = true;
if (meshTypeSelection.model.get(meshTypeSelection.currentIndex).type == "support_mesh")
{
settingPickDialog.additional_excluded_settings = base.all_categories_except_support;
}
else
{
settingPickDialog.additional_excluded_settings = []
}
}
}
}
@ -315,15 +373,18 @@ Item {
id: settingPickDialog
title: catalog.i18nc("@title:window", "Select Settings to Customize for this model")
width: screenScaleFactor * 360;
width: screenScaleFactor * 360
property string labelFilter: ""
property var additional_excluded_settings
onVisibilityChanged:
{
// force updating the model to sync it with addedSettingsModel
if(visible)
{
// Set skip setting, it will prevent from resetting selected mesh_type
contents.model.visibilityHandler.addSkipResetSetting(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type)
listview.model.forceUpdate()
}
}
@ -394,7 +455,12 @@ Item {
}
visibilityHandler: UM.SettingPreferenceVisibilityHandler {}
expanded: [ "*" ]
exclude: [ "machine_settings", "command_line_settings", "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ]
exclude:
{
var excluded_settings = [ "machine_settings", "command_line_settings", "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ];
excluded_settings = excluded_settings.concat(settingPickDialog.additional_excluded_settings);
return excluded_settings;
}
}
delegate:Loader
{

View file

@ -1,31 +1,36 @@
# Copyright (c) 2017 Ultimaker B.V.
# PluginBrowser is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QUrl, QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from UM.Application import Application
from UM.Qt.ListModel import ListModel
from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry
from UM.Qt.Bindings.PluginsModel import PluginsModel
from UM.Extension import Extension
from UM.i18n import i18nCatalog
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
from UM.PluginRegistry import PluginRegistry
from UM.Application import Application
from UM.Version import Version
from UM.Message import Message
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from PyQt5.QtCore import QUrl, QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot
import json
import os
import tempfile
import platform
import zipfile
import shutil
from cura.CuraApplication import CuraApplication
i18n_catalog = i18nCatalog("cura")
class PluginBrowser(QObject, Extension):
def __init__(self, parent=None):
super().__init__(parent)
self._api_version = 2
self._api_version = 4
self._api_url = "http://software.ultimaker.com/cura/v%s/" % self._api_version
self._plugin_list_request = None
@ -34,11 +39,18 @@ class PluginBrowser(QObject, Extension):
self._download_plugin_reply = None
self._network_manager = None
self._plugin_registry = Application.getInstance().getPluginRegistry()
self._plugins_metadata = []
self._plugins_model = None
# Can be 'installed' or 'availble'
self._view = "available"
self._restart_required = False
self._dialog = None
self._restartDialog = None
self._download_progress = 0
self._is_downloading = False
@ -52,16 +64,29 @@ class PluginBrowser(QObject, Extension):
)
]
# Installed plugins are really installed after reboot. In order to prevent the user from downloading the
# same file over and over again, we keep track of the upgraded plugins.
# Installed plugins are really installed after reboot. In order to
# prevent the user from downloading the same file over and over again,
# we keep track of the upgraded plugins.
# NOTE: This will be depreciated in favor of the 'status' system.
self._newly_installed_plugin_ids = []
self._newly_uninstalled_plugin_ids = []
self._plugin_statuses = {} # type: Dict[str, str]
# variables for the license agreement dialog
self._license_dialog_plugin_name = ""
self._license_dialog_license_content = ""
self._license_dialog_plugin_file_location = ""
self._restart_dialog_message = ""
showLicenseDialog = pyqtSignal()
showRestartDialog = pyqtSignal()
pluginsMetadataChanged = pyqtSignal()
onDownloadProgressChanged = pyqtSignal()
onIsDownloadingChanged = pyqtSignal()
restartRequiredChanged = pyqtSignal()
viewChanged = pyqtSignal()
@pyqtSlot(result = str)
def getLicenseDialogPluginName(self):
@ -75,15 +100,19 @@ class PluginBrowser(QObject, Extension):
def getLicenseDialogLicenseContent(self):
return self._license_dialog_license_content
@pyqtSlot(result = str)
def getRestartDialogMessage(self):
return self._restart_dialog_message
def openLicenseDialog(self, plugin_name, license_content, plugin_file_location):
self._license_dialog_plugin_name = plugin_name
self._license_dialog_license_content = license_content
self._license_dialog_plugin_file_location = plugin_file_location
self.showLicenseDialog.emit()
pluginsMetadataChanged = pyqtSignal()
onDownloadProgressChanged = pyqtSignal()
onIsDownloadingChanged = pyqtSignal()
def openRestartDialog(self, message):
self._restart_dialog_message = message
self.showRestartDialog.emit()
@pyqtProperty(bool, notify = onIsDownloadingChanged)
def isDownloading(self):
@ -179,17 +208,46 @@ class PluginBrowser(QObject, Extension):
@pyqtSlot(str)
def installPlugin(self, file_path):
# Ensure that it starts with a /, as otherwise it doesn't work on windows.
if not file_path.startswith("/"):
location = "/" + file_path # Ensure that it starts with a /, as otherwise it doesn't work on windows.
location = "/" + file_path
else:
location = file_path
result = PluginRegistry.getInstance().installPlugin("file://" + location)
self._newly_installed_plugin_ids.append(result["id"])
self.pluginsMetadataChanged.emit()
self.openRestartDialog(result["message"])
self._restart_required = True
self.restartRequiredChanged.emit()
# Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Plugin browser"), result["message"])
@pyqtSlot(str)
def removePlugin(self, plugin_id):
result = PluginRegistry.getInstance().uninstallPlugin(plugin_id)
self._newly_uninstalled_plugin_ids.append(result["id"])
self.pluginsMetadataChanged.emit()
self._restart_required = True
self.restartRequiredChanged.emit()
Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Plugin browser"), result["message"])
@pyqtSlot(str)
def enablePlugin(self, plugin_id):
self._plugin_registry.enablePlugin(plugin_id)
self.pluginsMetadataChanged.emit()
Logger.log("i", "%s was set as 'active'", id)
@pyqtSlot(str)
def disablePlugin(self, plugin_id):
self._plugin_registry.disablePlugin(plugin_id)
self.pluginsMetadataChanged.emit()
Logger.log("i", "%s was set as 'deactive'", id)
@pyqtProperty(int, notify = onDownloadProgressChanged)
def downloadProgress(self):
return self._download_progress
@ -221,53 +279,68 @@ class PluginBrowser(QObject, Extension):
self.setDownloadProgress(0)
self.setIsDownloading(False)
@pyqtSlot(str)
def setView(self, view):
self._view = view
self.viewChanged.emit()
self.pluginsMetadataChanged.emit()
@pyqtProperty(QObject, notify=pluginsMetadataChanged)
def pluginsModel(self):
if self._plugins_model is None:
self._plugins_model = ListModel()
self._plugins_model.addRoleName(Qt.UserRole + 1, "name")
self._plugins_model.addRoleName(Qt.UserRole + 2, "version")
self._plugins_model.addRoleName(Qt.UserRole + 3, "short_description")
self._plugins_model.addRoleName(Qt.UserRole + 4, "author")
self._plugins_model.addRoleName(Qt.UserRole + 5, "already_installed")
self._plugins_model.addRoleName(Qt.UserRole + 6, "file_location")
self._plugins_model.addRoleName(Qt.UserRole + 7, "can_upgrade")
else:
self._plugins_model.clear()
items = []
for metadata in self._plugins_metadata:
items.append({
"name": metadata["label"],
"version": metadata["version"],
"short_description": metadata["short_description"],
"author": metadata["author"],
"already_installed": self._checkAlreadyInstalled(metadata["id"]),
"file_location": metadata["file_location"],
"can_upgrade": self._checkCanUpgrade(metadata["id"], metadata["version"])
})
self._plugins_model.setItems(items)
self._plugins_model = PluginsModel(self._view)
# self._plugins_model.update()
# Check each plugin the registry for matching plugin from server
# metadata, and if found, compare the versions. Higher version sets
# 'can_upgrade' to 'True':
for plugin in self._plugins_model.items:
if self._checkCanUpgrade(plugin["id"], plugin["version"]):
plugin["can_upgrade"] = True
for item in self._plugins_metadata:
if item["id"] == plugin["id"]:
plugin["update_url"] = item["file_location"]
return self._plugins_model
def _checkCanUpgrade(self, id, version):
plugin_registry = PluginRegistry.getInstance()
metadata = plugin_registry.getMetaData(id)
if metadata != {}:
if id in self._newly_installed_plugin_ids:
return False # We already updated this plugin.
current_version = Version(metadata["plugin"]["version"])
new_version = Version(version)
if new_version > current_version:
# TODO: This could maybe be done more efficiently using a dictionary...
# Scan plugin server data for plugin with the given id:
for plugin in self._plugins_metadata:
if id == plugin["id"]:
reg_version = Version(version)
new_version = Version(plugin["version"])
if new_version > reg_version:
Logger.log("i", "%s has an update availible: %s", plugin["id"], plugin["version"])
return True
return False
def _checkAlreadyInstalled(self, id):
plugin_registry = PluginRegistry.getInstance()
metadata = plugin_registry.getMetaData(id)
if metadata != {}:
metadata = self._plugin_registry.getMetaData(id)
# We already installed this plugin, but the registry just doesn't know it yet.
if id in self._newly_installed_plugin_ids:
return True
# We already uninstalled this plugin, but the registry just doesn't know it yet:
elif id in self._newly_uninstalled_plugin_ids:
return False
elif metadata != {}:
return True
else:
if id in self._newly_installed_plugin_ids:
return True # We already installed this plugin, but the registry just doesn't know it yet.
return False
def _checkInstallStatus(self, plugin_id):
if plugin_id in self._plugin_registry.getInstalledPlugins():
return "installed"
else:
return "uninstalled"
def _checkEnabled(self, id):
if id in self._plugin_registry.getActivePlugins():
return True
return False
def _onRequestFinished(self, reply):
@ -290,7 +363,10 @@ class PluginBrowser(QObject, Extension):
if reply_url == self._api_url + "plugins":
try:
json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
# Add metadata to the manager:
self._plugins_metadata = json_data
self._plugin_registry.addExternalPlugins(self._plugins_metadata)
self.pluginsMetadataChanged.emit()
except json.decoder.JSONDecodeError:
Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
@ -316,3 +392,15 @@ class PluginBrowser(QObject, Extension):
self._network_manager = QNetworkAccessManager()
self._network_manager.finished.connect(self._onRequestFinished)
self._network_manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged)
@pyqtProperty(bool, notify=restartRequiredChanged)
def restartRequired(self):
return self._restart_required
@pyqtProperty(str, notify=viewChanged)
def viewing(self):
return self._view
@pyqtSlot()
def restart(self):
CuraApplication.getInstance().quit()

View file

@ -1,191 +1,209 @@
import UM 1.1 as UM
// Copyright (c) 2017 Ultimaker B.V.
// PluginBrowser is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2
import QtQuick.Controls 1.1
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
UM.Dialog
{
// TODO: Switch to QtQuick.Controls 2.x and remove QtQuick.Controls.Styles
import UM 1.1 as UM
Window {
id: base
title: catalog.i18nc("@title:window", "Find & Update plugins")
width: 600 * screenScaleFactor
height: 450 * screenScaleFactor
title: catalog.i18nc("@title:tab", "Plugins");
width: 800 * screenScaleFactor
height: 640 * screenScaleFactor
minimumWidth: 350 * screenScaleFactor
minimumHeight: 350 * screenScaleFactor
Item
{
anchors.fill: parent
Item
{
color: UM.Theme.getColor("sidebar")
Item {
id: view
anchors {
fill: parent
leftMargin: UM.Theme.getSize("default_margin").width
rightMargin: UM.Theme.getSize("default_margin").width
topMargin: UM.Theme.getSize("default_margin").height
bottomMargin: UM.Theme.getSize("default_margin").height
}
Rectangle {
id: topBar
height: childrenRect.height;
width: parent.width
Label
{
id: introText
text: catalog.i18nc("@label", "Here you can find a list of Third Party plugins.")
color: "transparent"
height: childrenRect.height
Row {
spacing: 12
height: childrenRect.height
width: childrenRect.width
anchors.horizontalCenter: parent.horizontalCenter
Button {
text: "Install"
style: ButtonStyle {
background: Rectangle {
color: "transparent"
implicitWidth: 96
implicitHeight: 48
Rectangle {
visible: manager.viewing == "available" ? true : false
color: UM.Theme.getColor("primary")
anchors.bottom: parent.bottom
width: parent.width
height: 30
height: 3
}
}
label: Text {
text: control.text
color: UM.Theme.getColor("text")
font {
pixelSize: 15
}
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}
onClicked: manager.setView("available")
}
Button
{
id: refresh
text: catalog.i18nc("@action:button", "Refresh")
onClicked: manager.requestPluginList()
anchors.right: parent.right
enabled: !manager.isDownloading
}
}
ScrollView
{
Button {
text: "Manage"
style: ButtonStyle {
background: Rectangle {
color: "transparent"
implicitWidth: 96
implicitHeight: 48
Rectangle {
visible: manager.viewing == "installed" ? true : false
color: UM.Theme.getColor("primary")
anchors.bottom: parent.bottom
width: parent.width
anchors.top: topBar.bottom
anchors.bottom: bottomBar.top
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
height: 3
}
}
label: Text {
text: control.text
color: UM.Theme.getColor("text")
font {
pixelSize: 15
}
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}
onClicked: manager.setView("installed")
}
}
}
// Scroll view breaks in QtQuick.Controls 2.x
ScrollView {
id: installedPluginList
width: parent.width
height: 400
anchors {
top: topBar.bottom
topMargin: UM.Theme.getSize("default_margin").height
bottom: bottomBar.top
bottomMargin: UM.Theme.getSize("default_margin").height
}
frameVisible: true
ListView
{
ListView {
id: pluginList
model: manager.pluginsModel
property var activePlugin
property var filter: "installed"
anchors.fill: parent
property var activePlugin
delegate: pluginDelegate
model: manager.pluginsModel
delegate: PluginEntry {}
}
}
Item
{
Rectangle {
id: bottomBar
width: parent.width
height: closeButton.height
height: childrenRect.height
color: "transparent"
anchors.bottom: parent.bottom
anchors.left: parent.left
ProgressBar
{
id: progressbar
anchors.bottom: parent.bottom
minimumValue: 0;
maximumValue: 100
anchors.left:parent.left
Label {
visible: manager.restartRequired
text: "You will need to restart Cura before changes in plugins have effect."
height: 30
verticalAlignment: Text.AlignVCenter
}
Button {
id: restartChangedButton
text: "Quit Cura"
anchors.right: closeButton.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width
value: manager.isDownloading ? manager.downloadProgress : 0
visible: manager.restartRequired
iconName: "dialog-restart"
onClicked: manager.restart()
style: ButtonStyle {
background: Rectangle {
implicitWidth: 96
implicitHeight: 30
color: UM.Theme.getColor("primary")
}
label: Text {
verticalAlignment: Text.AlignVCenter
color: UM.Theme.getColor("button_text")
font {
pixelSize: 13
bold: true
}
text: control.text
horizontalAlignment: Text.AlignHCenter
}
}
}
Button
{
Button {
id: closeButton
text: catalog.i18nc("@action:button", "Close")
iconName: "dialog-close"
onClicked:
{
if (manager.isDownloading)
{
onClicked: {
if ( manager.isDownloading ) {
manager.cancelDownload()
}
base.close();
}
anchors.bottom: parent.bottom
anchors.right: parent.right
style: ButtonStyle {
background: Rectangle {
color: "transparent"
implicitWidth: 96
implicitHeight: 30
border {
width: 1
color: UM.Theme.getColor("lining")
}
}
Item
{
SystemPalette { id: palette }
Component
{
id: pluginDelegate
Rectangle
{
width: pluginList.width;
height: texts.height;
color: index % 2 ? palette.base : palette.alternateBase
Column
{
id: texts
width: parent.width
height: childrenRect.height
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: downloadButton.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width
Label
{
text: "<b>" + model.name + "</b>" + ((model.author !== "") ? (" - " + model.author) : "")
width: contentWidth
height: contentHeight + UM.Theme.getSize("default_margin").height
label: Text {
verticalAlignment: Text.AlignVCenter
}
Label
{
text: model.short_description
width: parent.width
height: contentHeight + UM.Theme.getSize("default_margin").height
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
}
}
Button
{
id: downloadButton
text:
{
if (manager.isDownloading && pluginList.activePlugin == model)
{
return catalog.i18nc("@action:button", "Cancel");
}
else if (model.already_installed)
{
if (model.can_upgrade)
{
return catalog.i18nc("@action:button", "Upgrade");
}
return catalog.i18nc("@action:button", "Installed");
}
return catalog.i18nc("@action:button", "Download");
}
onClicked:
{
if(!manager.isDownloading)
{
pluginList.activePlugin = model;
manager.downloadAndInstallPlugin(model.file_location);
}
else
{
manager.cancelDownload();
}
}
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.verticalCenter: parent.verticalCenter
enabled:
{
if (manager.isDownloading)
{
return (pluginList.activePlugin == model);
}
else
{
return (!model.already_installed || model.can_upgrade);
color: UM.Theme.getColor("text")
text: control.text
horizontalAlignment: Text.AlignHCenter
}
}
}
}
}
}
UM.I18nCatalog { id: catalog; name: "cura" }
Connections
{
Connections {
target: manager
onShowLicenseDialog:
{
onShowLicenseDialog: {
licenseDialog.pluginName = manager.getLicenseDialogPluginName();
licenseDialog.licenseContent = manager.getLicenseDialogLicenseContent();
licenseDialog.pluginFileLocation = manager.getLicenseDialogPluginFileLocation();
@ -193,8 +211,7 @@ UM.Dialog
}
}
UM.Dialog
{
UM.Dialog {
id: licenseDialog
title: catalog.i18nc("@title:window", "Plugin License Agreement")
@ -258,5 +275,94 @@ UM.Dialog
}
]
}
Connections {
target: manager
onShowRestartDialog: {
restartDialog.message = manager.getRestartDialogMessage();
restartDialog.show();
}
}
Window {
id: restartDialog
// title: catalog.i18nc("@title:tab", "Plugins");
width: 360 * screenScaleFactor
height: 120 * screenScaleFactor
minimumWidth: 360 * screenScaleFactor
minimumHeight: 120 * screenScaleFactor
color: UM.Theme.getColor("sidebar")
property var message;
Text {
id: message
anchors {
left: parent.left
leftMargin: UM.Theme.getSize("default_margin").width
top: parent.top
topMargin: UM.Theme.getSize("default_margin").height
}
text: restartDialog.message != null ? restartDialog.message : ""
}
Button {
id: laterButton
text: "Later"
onClicked: restartDialog.close();
anchors {
left: parent.left
leftMargin: UM.Theme.getSize("default_margin").width
bottom: parent.bottom
bottomMargin: UM.Theme.getSize("default_margin").height
}
style: ButtonStyle {
background: Rectangle {
color: "transparent"
implicitWidth: 96
implicitHeight: 30
border {
width: 1
color: UM.Theme.getColor("lining")
}
}
label: Text {
verticalAlignment: Text.AlignVCenter
color: UM.Theme.getColor("text")
text: control.text
horizontalAlignment: Text.AlignHCenter
}
}
}
Button {
id: restartButton
text: "Quit Cura"
anchors {
right: parent.right
rightMargin: UM.Theme.getSize("default_margin").width
bottom: parent.bottom
bottomMargin: UM.Theme.getSize("default_margin").height
}
onClicked: manager.restart()
style: ButtonStyle {
background: Rectangle {
implicitWidth: 96
implicitHeight: 30
color: UM.Theme.getColor("primary")
}
label: Text {
verticalAlignment: Text.AlignVCenter
color: UM.Theme.getColor("button_text")
font {
pixelSize: 13
bold: true
}
text: control.text
horizontalAlignment: Text.AlignHCenter
}
}
}
}
}
}

View file

@ -0,0 +1,474 @@
// Copyright (c) 2017 Ultimaker B.V.
// PluginBrowser is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
// TODO: Switch to QtQuick.Controls 2.x and remove QtQuick.Controls.Styles
import UM 1.1 as UM
Component {
id: pluginDelegate
Rectangle {
// Don't show required plugins as they can't be managed anyway:
height: !model.required ? 84 : 0
visible: !model.required ? true : false
color: "transparent"
anchors {
left: parent.left
leftMargin: UM.Theme.getSize("default_margin").width
right: parent.right
rightMargin: UM.Theme.getSize("default_margin").width
}
// Bottom border:
Rectangle {
color: UM.Theme.getColor("lining")
width: parent.width
height: 1
anchors.bottom: parent.bottom
}
// Plugin info
Column {
id: pluginInfo
property var color: model.enabled ? UM.Theme.getColor("text") : UM.Theme.getColor("lining")
// Styling:
height: parent.height
anchors {
left: parent.left
top: parent.top
topMargin: UM.Theme.getSize("default_margin").height
right: authorInfo.left
rightMargin: UM.Theme.getSize("default_margin").width
}
Label {
text: model.name
width: parent.width
height: 24
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
font {
pixelSize: 13
bold: true
}
color: pluginInfo.color
}
Text {
text: model.description
width: parent.width
height: 36
clip: true
wrapMode: Text.WordWrap
color: pluginInfo.color
elide: Text.ElideRight
}
}
// Author info
Column {
id: authorInfo
width: 192
height: parent.height
anchors {
top: parent.top
topMargin: UM.Theme.getSize("default_margin").height
right: pluginActions.left
rightMargin: UM.Theme.getSize("default_margin").width
}
Label {
text: "<a href=\"mailto:"+model.author_email+"?Subject=Cura: "+model.name+"\">"+model.author+"</a>"
width: parent.width
height: 24
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
onLinkActivated: Qt.openUrlExternally("mailto:"+model.author_email+"?Subject=Cura: "+model.name+" Plugin")
color: model.enabled ? UM.Theme.getColor("text") : UM.Theme.getColor("lining")
}
}
// Plugin actions
Row {
id: pluginActions
width: 96
height: parent.height
anchors {
top: parent.top
right: parent.right
topMargin: UM.Theme.getSize("default_margin").height
}
layoutDirection: Qt.RightToLeft
spacing: UM.Theme.getSize("default_margin").width
// For 3rd-Party Plugins:
Button {
id: installButton
text: {
if ( manager.isDownloading && pluginList.activePlugin == model ) {
return catalog.i18nc( "@action:button", "Cancel" );
} else {
if (model.can_upgrade) {
return catalog.i18nc("@action:button", "Update");
}
return catalog.i18nc("@action:button", "Install");
}
}
visible: model.external && ((model.status !== "installed") || model.can_upgrade)
style: ButtonStyle {
background: Rectangle {
implicitWidth: 96
implicitHeight: 30
color: "transparent"
border {
width: 1
color: UM.Theme.getColor("lining")
}
}
label: Label {
text: control.text
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}
onClicked: {
if ( manager.isDownloading && pluginList.activePlugin == model ) {
manager.cancelDownload();
} else {
pluginList.activePlugin = model;
if ( model.can_upgrade ) {
manager.downloadAndInstallPlugin( model.update_url );
} else {
manager.downloadAndInstallPlugin( model.file_location );
}
}
}
}
Button {
id: removeButton
text: "Uninstall"
visible: model.can_uninstall && model.status == "installed"
enabled: !manager.isDownloading
style: ButtonStyle {
background: Rectangle {
implicitWidth: 96
implicitHeight: 30
color: "transparent"
border {
width: 1
color: UM.Theme.getColor("lining")
}
}
label: Text {
text: control.text
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}
onClicked: manager.removePlugin( model.id )
}
// For Ultimaker Plugins:
Button {
id: enableButton
text: "Enable"
visible: !model.external && model.enabled == false
style: ButtonStyle {
background: Rectangle {
implicitWidth: 96
implicitHeight: 30
color: "transparent"
border {
width: 1
color: UM.Theme.getColor("lining")
}
}
label: Text {
text: control.text
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}
onClicked: {
manager.enablePlugin(model.id);
}
}
Button {
id: disableButton
text: "Disable"
visible: !model.external && model.enabled == true
style: ButtonStyle {
background: Rectangle {
implicitWidth: 96
implicitHeight: 30
color: "transparent"
border {
width: 1
color: UM.Theme.getColor("lining")
}
}
label: Text {
text: control.text
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}
onClicked: {
manager.disablePlugin(model.id);
}
}
/*
Rectangle {
id: removeControls
visible: model.status == "installed" && model.enabled
width: 96
height: 30
color: "transparent"
Button {
id: removeButton
text: "Disable"
enabled: {
if ( manager.isDownloading && pluginList.activePlugin == model ) {
return false;
} else if ( model.required ) {
return false;
} else {
return true;
}
}
onClicked: {
manager.disablePlugin(model.id);
}
style: ButtonStyle {
background: Rectangle {
color: "white"
implicitWidth: 96
implicitHeight: 30
border {
width: 1
color: UM.Theme.getColor("lining")
}
}
label: Text {
verticalAlignment: Text.AlignVCenter
color: "grey"
text: control.text
horizontalAlignment: Text.AlignLeft
}
}
}
Button {
id: removeDropDown
property bool open: false
UM.RecolorImage {
anchors.centerIn: parent
height: 10
width: 10
source: UM.Theme.getIcon("arrow_bottom")
color: "grey"
}
enabled: {
if ( manager.isDownloading && pluginList.activePlugin == model ) {
return false;
} else if ( model.required ) {
return false;
} else {
return true;
}
}
anchors.right: parent.right
style: ButtonStyle {
background: Rectangle {
color: "transparent"
implicitWidth: 30
implicitHeight: 30
}
label: Text {
verticalAlignment: Text.AlignVCenter
color: "grey"
text: control.text
horizontalAlignment: Text.AlignHCenter
}
}
// For the disable option:
// onClicked: pluginList.model.setEnabled(model.id, checked)
onClicked: {
if ( !removeDropDown.open ) {
removeDropDown.open = true
}
else {
removeDropDown.open = false
}
}
}
Rectangle {
id: divider
width: 1
height: parent.height
anchors.right: removeDropDown.left
color: UM.Theme.getColor("lining")
}
Column {
id: options
anchors {
top: removeButton.bottom
left: parent.left
right: parent.right
}
height: childrenRect.height
visible: removeDropDown.open
Button {
id: disableButton
text: "Remove"
height: 30
width: parent.width
onClicked: {
removeDropDown.open = false;
manager.removePlugin( model.id );
}
}
}
}
*/
/*
Button {
id: enableButton
visible: !model.enabled && model.status == "installed"
onClicked: manager.enablePlugin( model.id );
text: "Enable"
style: ButtonStyle {
background: Rectangle {
color: "transparent"
implicitWidth: 96
implicitHeight: 30
border {
width: 1
color: UM.Theme.getColor("lining")
}
}
label: Text {
verticalAlignment: Text.AlignVCenter
color: UM.Theme.getColor("text")
text: control.text
horizontalAlignment: Text.AlignHCenter
}
}
}
Button {
id: updateButton
visible: model.status == "installed" && model.can_upgrade && model.enabled
// visible: model.already_installed
text: {
// If currently downloading:
if ( manager.isDownloading && pluginList.activePlugin == model ) {
return catalog.i18nc( "@action:button", "Cancel" );
} else {
return catalog.i18nc("@action:button", "Update");
}
}
style: ButtonStyle {
background: Rectangle {
color: UM.Theme.getColor("primary")
implicitWidth: 96
implicitHeight: 30
// radius: 4
}
label: Text {
verticalAlignment: Text.AlignVCenter
color: "white"
text: control.text
horizontalAlignment: Text.AlignHCenter
}
}
}
Button {
id: externalControls
visible: model.status == "available" ? true : false
text: {
// If currently downloading:
if ( manager.isDownloading && pluginList.activePlugin == model ) {
return catalog.i18nc( "@action:button", "Cancel" );
} else {
return catalog.i18nc("@action:button", "Install");
}
}
onClicked: {
if ( manager.isDownloading && pluginList.activePlugin == model ) {
manager.cancelDownload();
} else {
pluginList.activePlugin = model;
manager.downloadAndInstallPlugin( model.file_location );
}
}
style: ButtonStyle {
background: Rectangle {
color: "transparent"
implicitWidth: 96
implicitHeight: 30
border {
width: 1
color: UM.Theme.getColor("lining")
}
}
label: Text {
verticalAlignment: Text.AlignVCenter
color: "grey"
text: control.text
horizontalAlignment: Text.AlignHCenter
}
}
}
*/
ProgressBar {
id: progressbar
minimumValue: 0;
maximumValue: 100
anchors.left: installButton.left
anchors.right: installButton.right
anchors.top: installButton.bottom
anchors.topMargin: 4
value: manager.isDownloading ? manager.downloadProgress : 0
visible: manager.isDownloading && pluginList.activePlugin == model
style: ProgressBarStyle {
background: Rectangle {
color: "lightgray"
implicitHeight: 6
}
progress: Rectangle {
color: UM.Theme.getColor("primary")
}
}
}
}
}
}

View file

@ -0,0 +1,209 @@
# Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources
from UM.Application import Application
from UM.Extension import Extension
from UM.Logger import Logger
import os.path
import pkgutil
import sys
import importlib.util
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
## The post processing plugin is an Extension type plugin that enables pre-written scripts to post process generated
# g-code files.
class PostProcessingPlugin(QObject, Extension):
def __init__(self, parent = None):
super().__init__(parent)
self.addMenuItem(i18n_catalog.i18n("Modify G-Code"), self.showPopup)
self._view = None
# Loaded scripts are all scripts that can be used
self._loaded_scripts = {}
self._script_labels = {}
# Script list contains instances of scripts in loaded_scripts.
# There can be duplicates, which will be executed in sequence.
self._script_list = []
self._selected_script_index = -1
Application.getInstance().getOutputDeviceManager().writeStarted.connect(self.execute)
selectedIndexChanged = pyqtSignal()
@pyqtProperty("QVariant", notify = selectedIndexChanged)
def selectedScriptDefinitionId(self):
try:
return self._script_list[self._selected_script_index].getDefinitionId()
except:
return ""
@pyqtProperty("QVariant", notify=selectedIndexChanged)
def selectedScriptStackId(self):
try:
return self._script_list[self._selected_script_index].getStackId()
except:
return ""
## Execute all post-processing scripts on the gcode.
def execute(self, output_device):
scene = Application.getInstance().getController().getScene()
# If the scene does not have a gcode, do nothing
if not hasattr(scene, "gcode_dict"):
return
gcode_dict = getattr(scene, "gcode_dict")
if not gcode_dict:
return
# get gcode list for the active build plate
active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate
gcode_list = gcode_dict[active_build_plate_id]
if not gcode_list:
return
if ";POSTPROCESSED" not in gcode_list[0]:
for script in self._script_list:
try:
gcode_list = script.execute(gcode_list)
except Exception:
Logger.logException("e", "Exception in post-processing script.")
if len(self._script_list): # Add comment to g-code if any changes were made.
gcode_list[0] += ";POSTPROCESSED\n"
gcode_dict[active_build_plate_id] = gcode_list
setattr(scene, "gcode_dict", gcode_dict)
else:
Logger.log("e", "Already post processed")
@pyqtSlot(int)
def setSelectedScriptIndex(self, index):
self._selected_script_index = index
self.selectedIndexChanged.emit()
@pyqtProperty(int, notify = selectedIndexChanged)
def selectedScriptIndex(self):
return self._selected_script_index
@pyqtSlot(int, int)
def moveScript(self, index, new_index):
if new_index < 0 or new_index > len(self._script_list) - 1:
return # nothing needs to be done
else:
# Magical switch code.
self._script_list[new_index], self._script_list[index] = self._script_list[index], self._script_list[new_index]
self.scriptListChanged.emit()
self.selectedIndexChanged.emit() #Ensure that settings are updated
self._propertyChanged()
## Remove a script from the active script list by index.
@pyqtSlot(int)
def removeScriptByIndex(self, index):
self._script_list.pop(index)
if len(self._script_list) - 1 < self._selected_script_index:
self._selected_script_index = len(self._script_list) - 1
self.scriptListChanged.emit()
self.selectedIndexChanged.emit() # Ensure that settings are updated
self._propertyChanged()
## Load all scripts from provided path.
# This should probably only be done on init.
# \param path Path to check for scripts.
def loadAllScripts(self, path):
scripts = pkgutil.iter_modules(path = [path])
for loader, script_name, ispkg in scripts:
# Iterate over all scripts.
if script_name not in sys.modules:
spec = importlib.util.spec_from_file_location(__name__ + "." + script_name, os.path.join(path, script_name + ".py"))
loaded_script = importlib.util.module_from_spec(spec)
spec.loader.exec_module(loaded_script)
sys.modules[script_name] = loaded_script
loaded_class = getattr(loaded_script, script_name)
temp_object = loaded_class()
Logger.log("d", "Begin loading of script: %s", script_name)
try:
setting_data = temp_object.getSettingData()
if "name" in setting_data and "key" in setting_data:
self._script_labels[setting_data["key"]] = setting_data["name"]
self._loaded_scripts[setting_data["key"]] = loaded_class
else:
Logger.log("w", "Script %s.py has no name or key", script_name)
self._script_labels[script_name] = script_name
self._loaded_scripts[script_name] = loaded_class
except AttributeError:
Logger.log("e", "Script %s.py is not a recognised script type. Ensure it inherits Script", script_name)
except NotImplementedError:
Logger.log("e", "Script %s.py has no implemented settings", script_name)
self.loadedScriptListChanged.emit()
loadedScriptListChanged = pyqtSignal()
@pyqtProperty("QVariantList", notify = loadedScriptListChanged)
def loadedScriptList(self):
return sorted(list(self._loaded_scripts.keys()))
@pyqtSlot(str, result = str)
def getScriptLabelByKey(self, key):
return self._script_labels[key]
scriptListChanged = pyqtSignal()
@pyqtProperty("QVariantList", notify = scriptListChanged)
def scriptList(self):
script_list = [script.getSettingData()["key"] for script in self._script_list]
return script_list
@pyqtSlot(str)
def addScriptToList(self, key):
Logger.log("d", "Adding script %s to list.", key)
new_script = self._loaded_scripts[key]()
self._script_list.append(new_script)
self.setSelectedScriptIndex(len(self._script_list) - 1)
self.scriptListChanged.emit()
self._propertyChanged()
## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection.
def _createView(self):
Logger.log("d", "Creating post processing plugin view.")
## Load all scripts in the scripts folders
for root in [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Preferences)]:
try:
path = os.path.join(root, "scripts")
if not os.path.isdir(path):
try:
os.makedirs(path)
except OSError:
Logger.log("w", "Unable to create a folder for scripts: " + path)
continue
self.loadAllScripts(path)
except Exception as e:
Logger.logException("e", "Exception occurred while loading post processing plugin: {error_msg}".format(error_msg = str(e)))
# Create the plugin dialog component
path = os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml")
self._view = Application.getInstance().createQmlComponent(path, {"manager": self})
Logger.log("d", "Post processing view created.")
# Create the save button component
Application.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton"))
## Show the (GUI) popup of the post processing plugin.
def showPopup(self):
if self._view is None:
self._createView()
self._view.show()
## Property changed: trigger re-slice
# To do this we use the global container stack propertyChanged.
# Re-slicing is necessary for setting changes in this plugin, because the changes
# are applied only once per "fresh" gcode
def _propertyChanged(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
global_container_stack.propertyChanged.emit("post_processing_plugin", "value")

View file

@ -0,0 +1,501 @@
// Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V.
// The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
import QtQuick.Layouts 1.1
import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2
import UM 1.2 as UM
import Cura 1.0 as Cura
UM.Dialog
{
id: dialog
title: catalog.i18nc("@title:window", "Post Processing Plugin")
width: 700 * screenScaleFactor;
height: 500 * screenScaleFactor;
minimumWidth: 400 * screenScaleFactor;
minimumHeight: 250 * screenScaleFactor;
Item
{
UM.I18nCatalog{id: catalog; name:"cura"}
id: base
property int columnWidth: Math.floor((base.width / 2) - UM.Theme.getSize("default_margin").width)
property int textMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2)
property string activeScriptName
SystemPalette{ id: palette }
SystemPalette{ id: disabledPalette; colorGroup: SystemPalette.Disabled }
anchors.fill: parent
ExclusiveGroup
{
id: selectedScriptGroup
}
Item
{
id: activeScripts
anchors.left: parent.left
width: base.columnWidth
height: parent.height
Label
{
id: activeScriptsHeader
text: catalog.i18nc("@label", "Post Processing Scripts")
anchors.top: parent.top
anchors.topMargin: base.textMargin
anchors.left: parent.left
anchors.leftMargin: base.textMargin
anchors.right: parent.right
anchors.rightMargin: base.textMargin
font: UM.Theme.getFont("large")
}
ListView
{
id: activeScriptsList
anchors.top: activeScriptsHeader.bottom
anchors.topMargin: base.textMargin
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: parent.right
anchors.rightMargin: base.textMargin
height: childrenRect.height
model: manager.scriptList
delegate: Item
{
width: parent.width
height: activeScriptButton.height
Button
{
id: activeScriptButton
text: manager.getScriptLabelByKey(modelData.toString())
exclusiveGroup: selectedScriptGroup
checkable: true
checked: {
if (manager.selectedScriptIndex == index)
{
base.activeScriptName = manager.getScriptLabelByKey(modelData.toString())
return true
}
else
{
return false
}
}
onClicked:
{
forceActiveFocus()
manager.setSelectedScriptIndex(index)
base.activeScriptName = manager.getScriptLabelByKey(modelData.toString())
}
width: parent.width
height: UM.Theme.getSize("setting").height
style: ButtonStyle
{
background: Rectangle
{
color: activeScriptButton.checked ? palette.highlight : "transparent"
width: parent.width
height: parent.height
}
label: Label
{
wrapMode: Text.Wrap
text: control.text
color: activeScriptButton.checked ? palette.highlightedText : palette.text
}
}
}
Button
{
id: removeButton
text: "x"
width: 20 * screenScaleFactor
height: 20 * screenScaleFactor
anchors.right:parent.right
anchors.rightMargin: base.textMargin
anchors.verticalCenter: parent.verticalCenter
onClicked: manager.removeScriptByIndex(index)
style: ButtonStyle
{
label: Item
{
UM.RecolorImage
{
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: Math.floor(control.width / 2.7)
height: Math.floor(control.height / 2.7)
sourceSize.width: width
sourceSize.height: width
color: palette.text
source: UM.Theme.getIcon("cross1")
}
}
}
}
Button
{
id: downButton
text: ""
anchors.right: removeButton.left
anchors.verticalCenter: parent.verticalCenter
enabled: index != manager.scriptList.length - 1
width: 20 * screenScaleFactor
height: 20 * screenScaleFactor
onClicked:
{
if (manager.selectedScriptIndex == index)
{
manager.setSelectedScriptIndex(index + 1)
}
return manager.moveScript(index, index + 1)
}
style: ButtonStyle
{
label: Item
{
UM.RecolorImage
{
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: Math.floor(control.width / 2.5)
height: Math.floor(control.height / 2.5)
sourceSize.width: width
sourceSize.height: width
color: control.enabled ? palette.text : disabledPalette.text
source: UM.Theme.getIcon("arrow_bottom")
}
}
}
}
Button
{
id: upButton
text: ""
enabled: index != 0
width: 20 * screenScaleFactor
height: 20 * screenScaleFactor
anchors.right: downButton.left
anchors.verticalCenter: parent.verticalCenter
onClicked:
{
if (manager.selectedScriptIndex == index)
{
manager.setSelectedScriptIndex(index - 1)
}
return manager.moveScript(index, index - 1)
}
style: ButtonStyle
{
label: Item
{
UM.RecolorImage
{
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: Math.floor(control.width / 2.5)
height: Math.floor(control.height / 2.5)
sourceSize.width: width
sourceSize.height: width
color: control.enabled ? palette.text : disabledPalette.text
source: UM.Theme.getIcon("arrow_top")
}
}
}
}
}
}
Button
{
id: addButton
text: catalog.i18nc("@action", "Add a script")
anchors.left: parent.left
anchors.leftMargin: base.textMargin
anchors.top: activeScriptsList.bottom
anchors.topMargin: base.textMargin
menu: scriptsMenu
style: ButtonStyle
{
label: Label
{
text: control.text
}
}
}
Menu
{
id: scriptsMenu
Instantiator
{
model: manager.loadedScriptList
MenuItem
{
text: manager.getScriptLabelByKey(modelData.toString())
onTriggered: manager.addScriptToList(modelData.toString())
}
onObjectAdded: scriptsMenu.insertItem(index, object);
onObjectRemoved: scriptsMenu.removeItem(object);
}
}
}
Rectangle
{
color: UM.Theme.getColor("sidebar")
anchors.left: activeScripts.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: parent.right
height: parent.height
id: settingsPanel
Label
{
id: scriptSpecsHeader
text: manager.selectedScriptIndex == -1 ? catalog.i18nc("@label", "Settings") : base.activeScriptName
anchors.top: parent.top
anchors.topMargin: base.textMargin
anchors.left: parent.left
anchors.leftMargin: base.textMargin
anchors.right: parent.right
anchors.rightMargin: base.textMargin
height: 20 * screenScaleFactor
font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
}
ScrollView
{
id: scrollView
anchors.top: scriptSpecsHeader.bottom
anchors.topMargin: settingsPanel.textMargin
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
visible: manager.selectedScriptDefinitionId != ""
style: UM.Theme.styles.scrollview;
ListView
{
id: listview
spacing: UM.Theme.getSize("default_lining").height
model: UM.SettingDefinitionsModel
{
id: definitionsModel;
containerId: manager.selectedScriptDefinitionId
showAll: true
}
delegate:Loader
{
id: settingLoader
width: parent.width
height:
{
if(provider.properties.enabled == "True")
{
if(model.type != undefined)
{
return UM.Theme.getSize("section").height;
}
else
{
return 0;
}
}
else
{
return 0;
}
}
Behavior on height { NumberAnimation { duration: 100 } }
opacity: provider.properties.enabled == "True" ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 100 } }
enabled: opacity > 0
property var definition: model
property var settingDefinitionsModel: definitionsModel
property var propertyProvider: provider
property var globalPropertyProvider: inheritStackProvider
//Qt5.4.2 and earlier has a bug where this causes a crash: https://bugreports.qt.io/browse/QTBUG-35989
//In addition, while it works for 5.5 and higher, the ordering of the actual combo box drop down changes,
//causing nasty issues when selecting different options. So disable asynchronous loading of enum type completely.
asynchronous: model.type != "enum" && model.type != "extruder"
onLoaded: {
settingLoader.item.showRevertButton = false
settingLoader.item.showInheritButton = false
settingLoader.item.showLinkedSettingIcon = false
settingLoader.item.doDepthIndentation = true
settingLoader.item.doQualityUserSettingEmphasis = false
}
sourceComponent:
{
switch(model.type)
{
case "int":
return settingTextField
case "float":
return settingTextField
case "enum":
return settingComboBox
case "extruder":
return settingExtruder
case "bool":
return settingCheckBox
case "str":
return settingTextField
case "category":
return settingCategory
default:
return settingUnknown
}
}
UM.SettingPropertyProvider
{
id: provider
containerStackId: manager.selectedScriptStackId
key: model.key ? model.key : "None"
watchedProperties: [ "value", "enabled", "state", "validationState" ]
storeIndex: 0
}
// Specialty provider that only watches global_inherits (we cant filter on what property changed we get events
// so we bypass that to make a dedicated provider).
UM.SettingPropertyProvider
{
id: inheritStackProvider
containerStackId: Cura.MachineManager.activeMachineId
key: model.key ? model.key : "None"
watchedProperties: [ "limit_to_extruder" ]
}
Connections
{
target: item
onShowTooltip:
{
tooltip.text = text;
var position = settingLoader.mapToItem(settingsPanel, settingsPanel.x, 0);
tooltip.show(position);
tooltip.target.x = position.x + 1
}
onHideTooltip:
{
tooltip.hide();
}
}
}
}
}
}
Cura.SidebarTooltip
{
id: tooltip
}
Component
{
id: settingTextField;
Cura.SettingTextField { }
}
Component
{
id: settingComboBox;
Cura.SettingComboBox { }
}
Component
{
id: settingExtruder;
Cura.SettingExtruder { }
}
Component
{
id: settingCheckBox;
Cura.SettingCheckBox { }
}
Component
{
id: settingCategory;
Cura.SettingCategory { }
}
Component
{
id: settingUnknown;
Cura.SettingUnknown { }
}
}
rightButtons: Button
{
text: catalog.i18nc("@action:button", "Close")
iconName: "dialog-close"
onClicked: dialog.accept()
}
Button {
objectName: "postProcessingSaveAreaButton"
visible: activeScriptsList.count > 0
height: UM.Theme.getSize("save_button_save_to_button").height
width: height
tooltip: catalog.i18nc("@info:tooltip", "Change active post-processing scripts")
onClicked: dialog.show()
style: ButtonStyle {
background: Rectangle {
id: deviceSelectionIcon
border.width: UM.Theme.getSize("default_lining").width
border.color: !control.enabled ? UM.Theme.getColor("action_button_disabled_border") :
control.pressed ? UM.Theme.getColor("action_button_active_border") :
control.hovered ? UM.Theme.getColor("action_button_hovered_border") : UM.Theme.getColor("action_button_border")
color: !control.enabled ? UM.Theme.getColor("action_button_disabled") :
control.pressed ? UM.Theme.getColor("action_button_active") :
control.hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("action_button")
Behavior on color { ColorAnimation { duration: 50; } }
anchors.left: parent.left
anchors.leftMargin: Math.floor(UM.Theme.getSize("save_button_text_margin").width / 2);
width: parent.height
height: parent.height
UM.RecolorImage {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: Math.floor(parent.width / 2)
height: Math.floor(parent.height / 2)
sourceSize.width: width
sourceSize.height: height
color: !control.enabled ? UM.Theme.getColor("action_button_disabled_text") :
control.pressed ? UM.Theme.getColor("action_button_active_text") :
control.hovered ? UM.Theme.getColor("action_button_hovered_text") : UM.Theme.getColor("action_button_text");
source: "postprocessing.svg"
}
}
label: Label{ }
}
}
}

View file

@ -0,0 +1,2 @@
# PostProcessingPlugin
A post processing plugin for Cura

View file

@ -0,0 +1,111 @@
# Copyright (c) 2015 Jaime van Kessel
# Copyright (c) 2017 Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
from UM.Logger import Logger
from UM.Signal import Signal, signalemitter
from UM.i18n import i18nCatalog
# Setting stuff import
from UM.Application import Application
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Settings.ContainerRegistry import ContainerRegistry
import re
import json
import collections
i18n_catalog = i18nCatalog("cura")
## Base class for scripts. All scripts should inherit the script class.
@signalemitter
class Script:
def __init__(self):
super().__init__()
self._settings = None
self._stack = None
setting_data = self.getSettingData()
self._stack = ContainerStack(stack_id = str(id(self)))
self._stack.setDirty(False) # This stack does not need to be saved.
## Check if the definition of this script already exists. If not, add it to the registry.
if "key" in setting_data:
definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = setting_data["key"])
if definitions:
# Definition was found
self._definition = definitions[0]
else:
self._definition = DefinitionContainer(setting_data["key"])
self._definition.deserialize(json.dumps(setting_data))
ContainerRegistry.getInstance().addContainer(self._definition)
self._stack.addContainer(self._definition)
self._instance = InstanceContainer(container_id="ScriptInstanceContainer")
self._instance.setDefinition(self._definition.getId())
self._instance.addMetaDataEntry("setting_version", self._definition.getMetaDataEntry("setting_version", default = 0))
self._stack.addContainer(self._instance)
self._stack.propertyChanged.connect(self._onPropertyChanged)
ContainerRegistry.getInstance().addContainer(self._stack)
settingsLoaded = Signal()
valueChanged = Signal() # Signal emitted whenever a value of a setting is changed
def _onPropertyChanged(self, key, property_name):
if property_name == "value":
self.valueChanged.emit()
# Property changed: trigger reslice
# To do this we use the global container stack propertyChanged.
# Reslicing is necessary for setting changes in this plugin, because the changes
# are applied only once per "fresh" gcode
global_container_stack = Application.getInstance().getGlobalContainerStack()
global_container_stack.propertyChanged.emit(key, property_name)
## Needs to return a dict that can be used to construct a settingcategory file.
# See the example script for an example.
# It follows the same style / guides as the Uranium settings.
# Scripts can either override getSettingData directly, or use getSettingDataString
# to return a string that will be parsed as json. The latter has the benefit over
# returning a dict in that the order of settings is maintained.
def getSettingData(self):
setting_data = self.getSettingDataString()
if type(setting_data) == str:
setting_data = json.loads(setting_data, object_pairs_hook = collections.OrderedDict)
return setting_data
def getSettingDataString(self):
raise NotImplementedError()
def getDefinitionId(self):
if self._stack:
return self._stack.getBottom().getId()
def getStackId(self):
if self._stack:
return self._stack.getId()
## Convenience function that retrieves value of a setting from the stack.
def getSettingValueByKey(self, key):
return self._stack.getProperty(key, "value")
## Convenience function that finds the value in a line of g-code.
# When requesting key = x from line "G1 X100" the value 100 is returned.
def getValue(self, line, key, default = None):
if not key in line or (';' in line and line.find(key) > line.find(';')):
return default
sub_part = line[line.find(key) + 1:]
m = re.search('^-?[0-9]+\.?[0-9]*', sub_part)
if m is None:
return default
try:
return float(m.group(0))
except:
return default
## This is called when the script is executed.
# It gets a list of g-code strings and needs to return a (modified) list.
def execute(self, data):
raise NotImplementedError()

View file

@ -0,0 +1,11 @@
# Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
from . import PostProcessingPlugin
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {}
def register(app):
return {"extension": PostProcessingPlugin.PostProcessingPlugin()}

View file

@ -0,0 +1,8 @@
{
"name": "Post Processing",
"author": "Ultimaker",
"version": "2.2",
"api": 4,
"description": "Extension that allows for user created scripts for post processing",
"catalog": "cura"
}

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="512px"
height="512px"
viewBox="0 0 512 512"
style="enable-background:new 0 0 512 512;"
xml:space="preserve"
inkscape:version="0.91 r13725"
sodipodi:docname="postprocessing.svg"><metadata
id="metadata9"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs7" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1104"
inkscape:window-height="1006"
id="namedview5"
showgrid="false"
inkscape:zoom="1.3359375"
inkscape:cx="256"
inkscape:cy="256"
inkscape:window-x="701"
inkscape:window-y="121"
inkscape:window-maximized="0"
inkscape:current-layer="Layer_1" /><path
d="M 402.15234 0 C 371.74552 4.7369516e-015 345.79114 10.752017 324.21875 32.324219 C 302.57788 53.89652 291.82617 79.851497 291.82617 110.18945 C 291.82617 127.34315 295.26662 143.09419 302.16602 157.44531 L 238.38477 221.20312 C 227.77569 210.95036 218.04331 201.50935 209.66016 193.32422 C 207.33386 190.99792 202.68042 189.48707 198.60938 191.92969 L 191.74609 196.11719 C 165.34252 169.24836 154.17609 158.42965 150.57031 145.40234 C 146.84822 131.79345 150.22148 113.64862 153.71094 106.90234 C 156.61882 101.55183 165.69233 96.550326 173.36914 95.96875 L 183.37109 106.20508 C 185.69739 108.53139 189.30456 108.53139 191.63086 106.20508 L 227.57227 69.681641 C 229.89858 67.355335 229.89858 63.517712 227.57227 61.191406 L 169.53125 2.21875 C 167.20494 -0.10755598 163.48147 -0.10755598 161.27148 2.21875 L 125.33008 38.742188 C 123.00378 41.068494 123.00378 44.906116 125.33008 47.232422 L 129.16992 51.1875 C 129.16992 56.88695 128.35573 65.727167 123.70312 70.496094 C 116.49157 77.823958 102.18413 69.332919 92.878906 75.962891 C 83.689998 82.476548 72.05746 92.944493 64.613281 100.38867 C 57.285417 107.83285 29.138171 137.37722 9.015625 187.16016 C -11.106922 236.94311 4.3632369 283.12 15.296875 295.2168 C 21.11264 301.61414 31.696982 308.12804 29.835938 296.03125 C 27.974892 283.81815 24.951448 241.47942 38.792969 224.14844 C 52.634489 206.81746 70.894726 192.62799 94.623047 191.46484 C 117.42084 190.30169 130.56529 198.09417 160.10938 228.10352 L 156.85156 234.15234 C 154.75788 238.10706 155.92175 243.10728 158.24805 245.43359 C 161.95717 248.74082 172.37305 258.96006 186.52539 273.04297 L 6.9511719 452.54883 C 2.2984329 457.14417 1.1842379e-015 462.71497 0 469.14844 C -1.1842379e-015 475.69681 2.2984329 481.15473 6.9511719 485.51953 L 26.308594 505.22266 C 31.018838 509.76054 36.589603 512 42.908203 512 C 49.341623 512 54.800053 509.76054 59.337891 505.22266 L 238.96875 325.6582 C 317.6609 404.95524 424.21289 513.40234 424.21289 513.40234 L 482.25391 454.43164 C 437.71428 411.9686 358.71135 336.76293 291.93164 272.71484 L 354.68945 209.98047 C 369.08663 216.91399 384.90203 220.37891 402.15234 220.37891 C 425.29988 220.37891 446.52947 213.53073 465.77344 199.83398 C 485.08493 186.1372 498.57775 168.33291 506.31641 146.34961 C 510.08303 135.39222 512 126.69334 512 120.25586 C 512 117.79044 511.24662 115.80572 509.87695 114.16211 C 508.50726 112.5185 506.59041 111.69531 504.125 111.69531 C 502.61835 111.69531 496.86414 114.5734 486.72852 120.39453 C 476.6614 126.21564 465.50054 132.85752 453.37891 140.32227 C 441.18878 147.78698 434.7515 151.75888 433.92969 152.23828 L 386.40234 125.94141 L 386.40234 70.8125 L 458.51562 29.242188 C 461.18649 27.461587 462.48633 25.202356 462.48633 22.394531 C 462.48633 19.586706 461.1865 17.325625 458.51562 15.476562 C 451.32484 10.545729 442.4896 6.780346 432.08008 4.0410156 C 421.60206 1.3701797 411.67159 0 402.15234 0 z "
id="path3" /></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -0,0 +1,48 @@
from ..Script import Script
class BQ_PauseAtHeight(Script):
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name":"Pause at height (BQ Printers)",
"key": "BQ_PauseAtHeight",
"metadata":{},
"version": 2,
"settings":
{
"pause_height":
{
"label": "Pause height",
"description": "At what height should the pause occur",
"unit": "mm",
"type": "float",
"default_value": 5.0
}
}
}"""
def execute(self, data):
x = 0.
y = 0.
current_z = 0.
pause_z = self.getSettingValueByKey("pause_height")
for layer in data:
lines = layer.split("\n")
for line in lines:
if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0:
current_z = self.getValue(line, 'Z')
if current_z != None:
if current_z >= pause_z:
prepend_gcode = ";TYPE:CUSTOM\n"
prepend_gcode += "; -- Pause at height (%.2f mm) --\n" % pause_z
# Insert Pause gcode
prepend_gcode += "M25 ; Pauses the print and waits for the user to resume it\n"
index = data.index(layer)
layer = prepend_gcode + layer
data[index] = layer # Override the data of this layer with the modified data
return data
break
return data

View file

@ -0,0 +1,76 @@
# This PostProcessing Plugin script is released
# under the terms of the AGPLv3 or higher
from ..Script import Script
#from UM.Logger import Logger
# from cura.Settings.ExtruderManager import ExtruderManager
class ColorChange(Script):
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name":"Color Change",
"key": "ColorChange",
"metadata": {},
"version": 2,
"settings":
{
"layer_number":
{
"label": "Layer",
"description": "At what layer should color change occur. This will be before the layer starts printing. Specify multiple color changes with a comma.",
"unit": "",
"type": "str",
"default_value": "1"
},
"initial_retract":
{
"label": "Initial Retraction",
"description": "Initial filament retraction distance",
"unit": "mm",
"type": "float",
"default_value": 300.0
},
"later_retract":
{
"label": "Later Retraction Distance",
"description": "Later filament retraction distance for removal",
"unit": "mm",
"type": "float",
"default_value": 30.0
}
}
}"""
def execute(self, data: list):
"""data is a list. Each index contains a layer"""
layer_nums = self.getSettingValueByKey("layer_number")
initial_retract = self.getSettingValueByKey("initial_retract")
later_retract = self.getSettingValueByKey("later_retract")
color_change = "M600"
if initial_retract is not None and initial_retract > 0.:
color_change = color_change + (" E%.2f" % initial_retract)
if later_retract is not None and later_retract > 0.:
color_change = color_change + (" L%.2f" % later_retract)
color_change = color_change + " ; Generated by ColorChange plugin"
layer_targets = layer_nums.split(',')
if len(layer_targets) > 0:
for layer_num in layer_targets:
layer_num = int( layer_num.strip() )
if layer_num < len(data):
layer = data[ layer_num - 1 ]
lines = layer.split("\n")
lines.insert(2, color_change )
final_line = "\n".join( lines )
data[ layer_num - 1 ] = final_line
return data

View file

@ -0,0 +1,43 @@
# Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
from ..Script import Script
class ExampleScript(Script):
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name":"Example script",
"key": "ExampleScript",
"metadata": {},
"version": 2,
"settings":
{
"test":
{
"label": "Test",
"description": "None",
"unit": "mm",
"type": "float",
"default_value": 0.5,
"minimum_value": "0",
"minimum_value_warning": "0.1",
"maximum_value_warning": "1"
},
"derp":
{
"label": "zomg",
"description": "afgasgfgasfgasf",
"unit": "mm",
"type": "float",
"default_value": 0.5,
"minimum_value": "0",
"minimum_value_warning": "0.1",
"maximum_value_warning": "1"
}
}
}"""
def execute(self, data):
return data

View file

@ -0,0 +1,221 @@
from ..Script import Script
# from cura.Settings.ExtruderManager import ExtruderManager
class PauseAtHeight(Script):
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name":"Pause at height",
"key": "PauseAtHeight",
"metadata": {},
"version": 2,
"settings":
{
"pause_height":
{
"label": "Pause Height",
"description": "At what height should the pause occur",
"unit": "mm",
"type": "float",
"default_value": 5.0
},
"head_park_x":
{
"label": "Park Print Head X",
"description": "What X location does the head move to when pausing.",
"unit": "mm",
"type": "float",
"default_value": 190
},
"head_park_y":
{
"label": "Park Print Head Y",
"description": "What Y location does the head move to when pausing.",
"unit": "mm",
"type": "float",
"default_value": 190
},
"retraction_amount":
{
"label": "Retraction",
"description": "How much filament must be retracted at pause.",
"unit": "mm",
"type": "float",
"default_value": 0
},
"retraction_speed":
{
"label": "Retraction Speed",
"description": "How fast to retract the filament.",
"unit": "mm/s",
"type": "float",
"default_value": 25
},
"extrude_amount":
{
"label": "Extrude Amount",
"description": "How much filament should be extruded after pause. This is needed when doing a material change on Ultimaker2's to compensate for the retraction after the change. In that case 128+ is recommended.",
"unit": "mm",
"type": "float",
"default_value": 0
},
"extrude_speed":
{
"label": "Extrude Speed",
"description": "How fast to extrude the material after pause.",
"unit": "mm/s",
"type": "float",
"default_value": 3.3333
},
"redo_layers":
{
"label": "Redo Layers",
"description": "Redo a number of previous layers after a pause to increases adhesion.",
"unit": "layers",
"type": "int",
"default_value": 0
},
"standby_temperature":
{
"label": "Standby Temperature",
"description": "Change the temperature during the pause",
"unit": "°C",
"type": "int",
"default_value": 0
},
"resume_temperature":
{
"label": "Resume Temperature",
"description": "Change the temperature after the pause",
"unit": "°C",
"type": "int",
"default_value": 0
}
}
}"""
def execute(self, data: list):
"""data is a list. Each index contains a layer"""
x = 0.
y = 0.
current_z = 0.
pause_height = self.getSettingValueByKey("pause_height")
retraction_amount = self.getSettingValueByKey("retraction_amount")
retraction_speed = self.getSettingValueByKey("retraction_speed")
extrude_amount = self.getSettingValueByKey("extrude_amount")
extrude_speed = self.getSettingValueByKey("extrude_speed")
park_x = self.getSettingValueByKey("head_park_x")
park_y = self.getSettingValueByKey("head_park_y")
layers_started = False
redo_layers = self.getSettingValueByKey("redo_layers")
standby_temperature = self.getSettingValueByKey("standby_temperature")
resume_temperature = self.getSettingValueByKey("resume_temperature")
# T = ExtruderManager.getInstance().getActiveExtruderStack().getProperty("material_print_temperature", "value")
# with open("out.txt", "w") as f:
# f.write(T)
# use offset to calculate the current height: <current_height> = <current_z> - <layer_0_z>
layer_0_z = 0.
got_first_g_cmd_on_layer_0 = False
for layer in data:
lines = layer.split("\n")
for line in lines:
if ";LAYER:0" in line:
layers_started = True
continue
if not layers_started:
continue
if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0:
current_z = self.getValue(line, 'Z')
if not got_first_g_cmd_on_layer_0:
layer_0_z = current_z
got_first_g_cmd_on_layer_0 = True
x = self.getValue(line, 'X', x)
y = self.getValue(line, 'Y', y)
if current_z is not None:
current_height = current_z - layer_0_z
if current_height >= pause_height:
index = data.index(layer)
prevLayer = data[index - 1]
prevLines = prevLayer.split("\n")
current_e = 0.
for prevLine in reversed(prevLines):
current_e = self.getValue(prevLine, 'E', -1)
if current_e >= 0:
break
# include a number of previous layers
for i in range(1, redo_layers + 1):
prevLayer = data[index - i]
layer = prevLayer + layer
prepend_gcode = ";TYPE:CUSTOM\n"
prepend_gcode += ";added code by post processing\n"
prepend_gcode += ";script: PauseAtHeight.py\n"
prepend_gcode += ";current z: %f \n" % current_z
prepend_gcode += ";current height: %f \n" % current_height
# Retraction
prepend_gcode += "M83\n"
if retraction_amount != 0:
prepend_gcode += "G1 E-%f F%f\n" % (retraction_amount, retraction_speed * 60)
# Move the head away
prepend_gcode += "G1 Z%f F300\n" % (current_z + 1)
prepend_gcode += "G1 X%f Y%f F9000\n" % (park_x, park_y)
if current_z < 15:
prepend_gcode += "G1 Z15 F300\n"
# Disable the E steppers
prepend_gcode += "M84 E0\n"
# Set extruder standby temperature
prepend_gcode += "M104 S%i; standby temperature\n" % (standby_temperature)
# Wait till the user continues printing
prepend_gcode += "M0 ;Do the actual pause\n"
# Set extruder resume temperature
prepend_gcode += "M109 S%i; resume temperature\n" % (resume_temperature)
# Push the filament back,
if retraction_amount != 0:
prepend_gcode += "G1 E%f F%f\n" % (retraction_amount, retraction_speed * 60)
# Optionally extrude material
if extrude_amount != 0:
prepend_gcode += "G1 E%f F%f\n" % (extrude_amount, extrude_speed * 60)
# and retract again, the properly primes the nozzle
# when changing filament.
if retraction_amount != 0:
prepend_gcode += "G1 E-%f F%f\n" % (retraction_amount, retraction_speed * 60)
# Move the head back
prepend_gcode += "G1 Z%f F300\n" % (current_z + 1)
prepend_gcode += "G1 X%f Y%f F9000\n" % (x, y)
if retraction_amount != 0:
prepend_gcode += "G1 E%f F%f\n" % (retraction_amount, retraction_speed * 60)
prepend_gcode += "G1 F9000\n"
prepend_gcode += "M82\n"
# reset extrude value to pre pause value
prepend_gcode += "G92 E%f\n" % (current_e)
layer = prepend_gcode + layer
# Override the data of this layer with the
# modified data
data[index] = layer
return data
break
return data

View file

@ -0,0 +1,169 @@
from ..Script import Script
class PauseAtHeightforRepetier(Script):
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name":"Pause at height for repetier",
"key": "PauseAtHeightforRepetier",
"metadata": {},
"version": 2,
"settings":
{
"pause_height":
{
"label": "Pause height",
"description": "At what height should the pause occur",
"unit": "mm",
"type": "float",
"default_value": 5.0
},
"head_park_x":
{
"label": "Park print head X",
"description": "What x location does the head move to when pausing.",
"unit": "mm",
"type": "float",
"default_value": 5.0
},
"head_park_y":
{
"label": "Park print head Y",
"description": "What y location does the head move to when pausing.",
"unit": "mm",
"type": "float",
"default_value": 5.0
},
"head_move_Z":
{
"label": "Head move Z",
"description": "The Hieght of Z-axis retraction before parking.",
"unit": "mm",
"type": "float",
"default_value": 15.0
},
"retraction_amount":
{
"label": "Retraction",
"description": "How much fillament must be retracted at pause.",
"unit": "mm",
"type": "float",
"default_value": 5.0
},
"extrude_amount":
{
"label": "Extrude amount",
"description": "How much filament should be extruded after pause. This is needed when doing a material change on Ultimaker2's to compensate for the retraction after the change. In that case 128+ is recommended.",
"unit": "mm",
"type": "float",
"default_value": 90.0
},
"redo_layers":
{
"label": "Redo layers",
"description": "Redo a number of previous layers after a pause to increases adhesion.",
"unit": "layers",
"type": "int",
"default_value": 0
}
}
}"""
def execute(self, data):
x = 0.
y = 0.
current_z = 0.
pause_z = self.getSettingValueByKey("pause_height")
retraction_amount = self.getSettingValueByKey("retraction_amount")
extrude_amount = self.getSettingValueByKey("extrude_amount")
park_x = self.getSettingValueByKey("head_park_x")
park_y = self.getSettingValueByKey("head_park_y")
move_Z = self.getSettingValueByKey("head_move_Z")
layers_started = False
redo_layers = self.getSettingValueByKey("redo_layers")
for layer in data:
lines = layer.split("\n")
for line in lines:
if ";LAYER:0" in line:
layers_started = True
continue
if not layers_started:
continue
if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0:
current_z = self.getValue(line, 'Z')
x = self.getValue(line, 'X', x)
y = self.getValue(line, 'Y', y)
if current_z != None:
if current_z >= pause_z:
index = data.index(layer)
prevLayer = data[index-1]
prevLines = prevLayer.split("\n")
current_e = 0.
for prevLine in reversed(prevLines):
current_e = self.getValue(prevLine, 'E', -1)
if current_e >= 0:
break
prepend_gcode = ";TYPE:CUSTOM\n"
prepend_gcode += ";added code by post processing\n"
prepend_gcode += ";script: PauseAtHeightforRepetier.py\n"
prepend_gcode += ";current z: %f \n" % (current_z)
prepend_gcode += ";current X: %f \n" % (x)
prepend_gcode += ";current Y: %f \n" % (y)
#Retraction
prepend_gcode += "M83\n"
if retraction_amount != 0:
prepend_gcode += "G1 E-%f F6000\n" % (retraction_amount)
#Move the head away
prepend_gcode += "G1 Z%f F300\n" % (1 + current_z)
prepend_gcode += "G1 X%f Y%f F9000\n" % (park_x, park_y)
if current_z < move_Z:
prepend_gcode += "G1 Z%f F300\n" % (current_z + move_Z)
#Disable the E steppers
prepend_gcode += "M84 E0\n"
#Wait till the user continues printing
prepend_gcode += "@pause now change filament and press continue printing ;Do the actual pause\n"
#Push the filament back,
if retraction_amount != 0:
prepend_gcode += "G1 E%f F6000\n" % (retraction_amount)
# Optionally extrude material
if extrude_amount != 0:
prepend_gcode += "G1 E%f F200\n" % (extrude_amount)
prepend_gcode += "@info wait for cleaning nozzle from previous filament\n"
prepend_gcode += "@pause remove the waste filament from parking area and press continue printing\n"
# and retract again, the properly primes the nozzle when changing filament.
if retraction_amount != 0:
prepend_gcode += "G1 E-%f F6000\n" % (retraction_amount)
#Move the head back
prepend_gcode += "G1 Z%f F300\n" % (1 + current_z)
prepend_gcode +="G1 X%f Y%f F9000\n" % (x, y)
if retraction_amount != 0:
prepend_gcode +="G1 E%f F6000\n" % (retraction_amount)
prepend_gcode +="G1 F9000\n"
prepend_gcode +="M82\n"
# reset extrude value to pre pause value
prepend_gcode +="G92 E%f\n" % (current_e)
layer = prepend_gcode + layer
# include a number of previous layers
for i in range(1, redo_layers + 1):
prevLayer = data[index-i]
layer = prevLayer + layer
data[index] = layer #Override the data of this layer with the modified data
return data
break
return data

View file

@ -0,0 +1,56 @@
# Copyright (c) 2017 Ruben Dulek
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
import re #To perform the search and replace.
from ..Script import Script
## Performs a search-and-replace on all g-code.
#
# Due to technical limitations, the search can't cross the border between
# layers.
class SearchAndReplace(Script):
def getSettingDataString(self):
return """{
"name": "Search and Replace",
"key": "SearchAndReplace",
"metadata": {},
"version": 2,
"settings":
{
"search":
{
"label": "Search",
"description": "All occurrences of this text will get replaced by the replacement text.",
"type": "str",
"default_value": ""
},
"replace":
{
"label": "Replace",
"description": "The search text will get replaced by this text.",
"type": "str",
"default_value": ""
},
"is_regex":
{
"label": "Use Regular Expressions",
"description": "When enabled, the search text will be interpreted as a regular expression.",
"type": "bool",
"default_value": false
}
}
}"""
def execute(self, data):
search_string = self.getSettingValueByKey("search")
if not self.getSettingValueByKey("is_regex"):
search_string = re.escape(search_string) #Need to search for the actual string, not as a regex.
search_regex = re.compile(search_string)
replace_string = self.getSettingValueByKey("replace")
for layer_number, layer in enumerate(data):
data[layer_number] = re.sub(search_regex, replace_string, layer) #Replace all.
return data

View file

@ -0,0 +1,469 @@
# This PostProcessingPlugin script is released under the terms of the AGPLv3 or higher.
"""
Copyright (c) 2017 Christophe Baribaud 2017
Python implementation of https://github.com/electrocbd/post_stretch
Correction of hole sizes, cylinder diameters and curves
See the original description in https://github.com/electrocbd/post_stretch
WARNING This script has never been tested with several extruders
"""
from ..Script import Script
import numpy as np
from UM.Logger import Logger
from UM.Application import Application
import re
def _getValue(line, key, default=None):
"""
Convenience function that finds the value in a line of g-code.
When requesting key = x from line "G1 X100" the value 100 is returned.
It is a copy of Stript's method, so it is no DontRepeatYourself, but
I split the class into setup part (Stretch) and execution part (Strecher)
and only the setup part inherits from Script
"""
if not key in line or (";" in line and line.find(key) > line.find(";")):
return default
sub_part = line[line.find(key) + 1:]
number = re.search(r"^-?[0-9]+\.?[0-9]*", sub_part)
if number is None:
return default
return float(number.group(0))
class GCodeStep():
"""
Class to store the current value of each G_Code parameter
for any G-Code step
"""
def __init__(self, step):
self.step = step
self.step_x = 0
self.step_y = 0
self.step_z = 0
self.step_e = 0
self.step_f = 0
self.comment = ""
def readStep(self, line):
"""
Reads gcode from line into self
"""
self.step_x = _getValue(line, "X", self.step_x)
self.step_y = _getValue(line, "Y", self.step_y)
self.step_z = _getValue(line, "Z", self.step_z)
self.step_e = _getValue(line, "E", self.step_e)
self.step_f = _getValue(line, "F", self.step_f)
return
def copyPosFrom(self, step):
"""
Copies positions of step into self
"""
self.step_x = step.step_x
self.step_y = step.step_y
self.step_z = step.step_z
self.step_e = step.step_e
self.step_f = step.step_f
self.comment = step.comment
return
# Execution part of the stretch plugin
class Stretcher():
"""
Execution part of the stretch algorithm
"""
def __init__(self, line_width, wc_stretch, pw_stretch):
self.line_width = line_width
self.wc_stretch = wc_stretch
self.pw_stretch = pw_stretch
if self.pw_stretch > line_width / 4:
self.pw_stretch = line_width / 4 # Limit value of pushwall stretch distance
self.outpos = GCodeStep(0)
self.vd1 = np.empty((0, 2)) # Start points of segments
# of already deposited material for current layer
self.vd2 = np.empty((0, 2)) # End points of segments
# of already deposited material for current layer
self.layer_z = 0 # Z position of the extrusion moves of the current layer
self.layergcode = ""
def execute(self, data):
"""
Computes the new X and Y coordinates of all g-code steps
"""
Logger.log("d", "Post stretch with line width = " + str(self.line_width)
+ "mm wide circle stretch = " + str(self.wc_stretch)+ "mm"
+ "and push wall stretch = " + str(self.pw_stretch) + "mm")
retdata = []
layer_steps = []
current = GCodeStep(0)
self.layer_z = 0.
current_e = 0.
for layer in data:
lines = layer.rstrip("\n").split("\n")
for line in lines:
current.comment = ""
if line.find(";") >= 0:
current.comment = line[line.find(";"):]
if _getValue(line, "G") == 0:
current.readStep(line)
onestep = GCodeStep(0)
onestep.copyPosFrom(current)
elif _getValue(line, "G") == 1:
current.readStep(line)
onestep = GCodeStep(1)
onestep.copyPosFrom(current)
elif _getValue(line, "G") == 92:
current.readStep(line)
onestep = GCodeStep(-1)
onestep.copyPosFrom(current)
else:
onestep = GCodeStep(-1)
onestep.copyPosFrom(current)
onestep.comment = line
if line.find(";LAYER:") >= 0 and len(layer_steps):
# Previous plugin "forgot" to separate two layers...
Logger.log("d", "Layer Z " + "{:.3f}".format(self.layer_z)
+ " " + str(len(layer_steps)) + " steps")
retdata.append(self.processLayer(layer_steps))
layer_steps = []
layer_steps.append(onestep)
# self.layer_z is the z position of the last extrusion move (not travel move)
if current.step_z != self.layer_z and current.step_e != current_e:
self.layer_z = current.step_z
current_e = current.step_e
if len(layer_steps): # Force a new item in the array
Logger.log("d", "Layer Z " + "{:.3f}".format(self.layer_z)
+ " " + str(len(layer_steps)) + " steps")
retdata.append(self.processLayer(layer_steps))
layer_steps = []
retdata.append(";Wide circle stretch distance " + str(self.wc_stretch) + "\n")
retdata.append(";Push wall stretch distance " + str(self.pw_stretch) + "\n")
return retdata
def extrusionBreak(self, layer_steps, i_pos):
"""
Returns true if the command layer_steps[i_pos] breaks the extruded filament
i.e. it is a travel move
"""
if i_pos == 0:
return True # Begining a layer always breaks filament (for simplicity)
step = layer_steps[i_pos]
prev_step = layer_steps[i_pos - 1]
if step.step_e != prev_step.step_e:
return False
delta_x = step.step_x - prev_step.step_x
delta_y = step.step_y - prev_step.step_y
if delta_x * delta_x + delta_y * delta_y < self.line_width * self.line_width / 4:
# This is a very short movement, less than 0.5 * line_width
# It does not break filament, we should stay in the same extrusion sequence
return False
return True # New sequence
def processLayer(self, layer_steps):
"""
Computes the new coordinates of g-code steps
for one layer (all the steps at the same Z coordinate)
"""
self.outpos.step_x = -1000 # Force output of X and Y coordinates
self.outpos.step_y = -1000 # at each start of layer
self.layergcode = ""
self.vd1 = np.empty((0, 2))
self.vd2 = np.empty((0, 2))
orig_seq = np.empty((0, 2))
modif_seq = np.empty((0, 2))
iflush = 0
for i, step in enumerate(layer_steps):
if step.step == 0 or step.step == 1:
if self.extrusionBreak(layer_steps, i):
# No extrusion since the previous step, so it is a travel move
# Let process steps accumulated into orig_seq,
# which are a sequence of continuous extrusion
modif_seq = np.copy(orig_seq)
if len(orig_seq) >= 2:
self.workOnSequence(orig_seq, modif_seq)
self.generate(layer_steps, iflush, i, modif_seq)
iflush = i
orig_seq = np.empty((0, 2))
orig_seq = np.concatenate([orig_seq, np.array([[step.step_x, step.step_y]])])
if len(orig_seq):
modif_seq = np.copy(orig_seq)
if len(orig_seq) >= 2:
self.workOnSequence(orig_seq, modif_seq)
self.generate(layer_steps, iflush, len(layer_steps), modif_seq)
return self.layergcode
def stepToGcode(self, onestep):
"""
Converts a step into G-Code
For each of the X, Y, Z, E and F parameter,
the parameter is written only if its value changed since the
previous g-code step.
"""
sout = ""
if onestep.step_f != self.outpos.step_f:
self.outpos.step_f = onestep.step_f
sout += " F{:.0f}".format(self.outpos.step_f).rstrip(".")
if onestep.step_x != self.outpos.step_x or onestep.step_y != self.outpos.step_y:
assert onestep.step_x >= -1000 and onestep.step_x < 1000 # If this assertion fails,
# something went really wrong !
self.outpos.step_x = onestep.step_x
sout += " X{:.3f}".format(self.outpos.step_x).rstrip("0").rstrip(".")
assert onestep.step_y >= -1000 and onestep.step_y < 1000 # If this assertion fails,
# something went really wrong !
self.outpos.step_y = onestep.step_y
sout += " Y{:.3f}".format(self.outpos.step_y).rstrip("0").rstrip(".")
if onestep.step_z != self.outpos.step_z or onestep.step_z != self.layer_z:
self.outpos.step_z = onestep.step_z
sout += " Z{:.3f}".format(self.outpos.step_z).rstrip("0").rstrip(".")
if onestep.step_e != self.outpos.step_e:
self.outpos.step_e = onestep.step_e
sout += " E{:.5f}".format(self.outpos.step_e).rstrip("0").rstrip(".")
return sout
def generate(self, layer_steps, ibeg, iend, orig_seq):
"""
Appends g-code lines to the plugin's returned string
starting from step ibeg included and until step iend excluded
"""
ipos = 0
for i in range(ibeg, iend):
if layer_steps[i].step == 0:
layer_steps[i].step_x = orig_seq[ipos][0]
layer_steps[i].step_y = orig_seq[ipos][1]
sout = "G0" + self.stepToGcode(layer_steps[i])
self.layergcode = self.layergcode + sout + "\n"
ipos = ipos + 1
elif layer_steps[i].step == 1:
layer_steps[i].step_x = orig_seq[ipos][0]
layer_steps[i].step_y = orig_seq[ipos][1]
sout = "G1" + self.stepToGcode(layer_steps[i])
self.layergcode = self.layergcode + sout + "\n"
ipos = ipos + 1
else:
self.layergcode = self.layergcode + layer_steps[i].comment + "\n"
def workOnSequence(self, orig_seq, modif_seq):
"""
Computes new coordinates for a sequence
A sequence is a list of consecutive g-code steps
of continuous material extrusion
"""
d_contact = self.line_width / 2.0
if (len(orig_seq) > 2 and
((orig_seq[len(orig_seq) - 1] - orig_seq[0]) ** 2).sum(0) < d_contact * d_contact):
# Starting and ending point of the sequence are nearby
# It is a closed loop
#self.layergcode = self.layergcode + ";wideCircle\n"
self.wideCircle(orig_seq, modif_seq)
else:
#self.layergcode = self.layergcode + ";wideTurn\n"
self.wideTurn(orig_seq, modif_seq) # It is an open curve
if len(orig_seq) > 6: # Don't try push wall on a short sequence
self.pushWall(orig_seq, modif_seq)
if len(orig_seq):
self.vd1 = np.concatenate([self.vd1, np.array(orig_seq[:-1])])
self.vd2 = np.concatenate([self.vd2, np.array(orig_seq[1:])])
def wideCircle(self, orig_seq, modif_seq):
"""
Similar to wideTurn
The first and last point of the sequence are the same,
so it is possible to extend the end of the sequence
with its beginning when seeking for triangles
It is necessary to find the direction of the curve, knowing three points (a triangle)
If the triangle is not wide enough, there is a huge risk of finding
an incorrect orientation, due to insufficient accuracy.
So, when the consecutive points are too close, the method
use following and preceding points to form a wider triangle around
the current point
dmin_tri is the minimum distance between two consecutive points
of an acceptable triangle
"""
dmin_tri = self.line_width / 2.0
iextra_base = np.floor_divide(len(orig_seq), 3) # Nb of extra points
ibeg = 0 # Index of first point of the triangle
iend = 0 # Index of the third point of the triangle
for i, step in enumerate(orig_seq):
if i == 0 or i == len(orig_seq) - 1:
# First and last point of the sequence are the same,
# so it is necessary to skip one of these two points
# when creating a triangle containing the first or the last point
iextra = iextra_base + 1
else:
iextra = iextra_base
# i is the index of the second point of the triangle
# pos_after is the array of positions of the original sequence
# after the current point
pos_after = np.resize(np.roll(orig_seq, -i-1, 0), (iextra, 2))
# Vector of distances between the current point and each following point
dist_from_point = ((step - pos_after) ** 2).sum(1)
if np.amax(dist_from_point) < dmin_tri * dmin_tri:
continue
iend = np.argmax(dist_from_point >= dmin_tri * dmin_tri)
# pos_before is the array of positions of the original sequence
# before the current point
pos_before = np.resize(np.roll(orig_seq, -i, 0)[::-1], (iextra, 2))
# This time, vector of distances between the current point and each preceding point
dist_from_point = ((step - pos_before) ** 2).sum(1)
if np.amax(dist_from_point) < dmin_tri * dmin_tri:
continue
ibeg = np.argmax(dist_from_point >= dmin_tri * dmin_tri)
# See https://github.com/electrocbd/post_stretch for explanations
# relpos is the relative position of the projection of the second point
# of the triangle on the segment from the first to the third point
# 0 means the position of the first point, 1 means the position of the third,
# intermediate values are positions between
length_base = ((pos_after[iend] - pos_before[ibeg]) ** 2).sum(0)
relpos = ((step - pos_before[ibeg])
* (pos_after[iend] - pos_before[ibeg])).sum(0)
if np.fabs(relpos) < 1000.0 * np.fabs(length_base):
relpos /= length_base
else:
relpos = 0.5 # To avoid division by zero or precision loss
projection = (pos_before[ibeg] + relpos * (pos_after[iend] - pos_before[ibeg]))
dist_from_proj = np.sqrt(((projection - step) ** 2).sum(0))
if dist_from_proj > 0.001: # Move central point only if points are not aligned
modif_seq[i] = (step - (self.wc_stretch / dist_from_proj)
* (projection - step))
return
def wideTurn(self, orig_seq, modif_seq):
'''
We have to select three points in order to form a triangle
These three points should be far enough from each other to have
a reliable estimation of the orientation of the current turn
'''
dmin_tri = self.line_width / 2.0
ibeg = 0
iend = 2
for i in range(1, len(orig_seq) - 1):
dist_from_point = ((orig_seq[i] - orig_seq[i+1:]) ** 2).sum(1)
if np.amax(dist_from_point) < dmin_tri * dmin_tri:
continue
iend = i + 1 + np.argmax(dist_from_point >= dmin_tri * dmin_tri)
dist_from_point = ((orig_seq[i] - orig_seq[i-1::-1]) ** 2).sum(1)
if np.amax(dist_from_point) < dmin_tri * dmin_tri:
continue
ibeg = i - 1 - np.argmax(dist_from_point >= dmin_tri * dmin_tri)
length_base = ((orig_seq[iend] - orig_seq[ibeg]) ** 2).sum(0)
relpos = ((orig_seq[i] - orig_seq[ibeg]) * (orig_seq[iend] - orig_seq[ibeg])).sum(0)
if np.fabs(relpos) < 1000.0 * np.fabs(length_base):
relpos /= length_base
else:
relpos = 0.5
projection = orig_seq[ibeg] + relpos * (orig_seq[iend] - orig_seq[ibeg])
dist_from_proj = np.sqrt(((projection - orig_seq[i]) ** 2).sum(0))
if dist_from_proj > 0.001:
modif_seq[i] = (orig_seq[i] - (self.wc_stretch / dist_from_proj)
* (projection - orig_seq[i]))
return
def pushWall(self, orig_seq, modif_seq):
"""
The algorithm tests for each segment if material was
already deposited at one or the other side of this segment.
If material was deposited at one side but not both,
the segment is moved into the direction of the deposited material,
to "push the wall"
Already deposited material is stored as segments.
vd1 is the array of the starting points of the segments
vd2 is the array of the ending points of the segments
For example, segment nr 8 starts at position self.vd1[8]
and ends at position self.vd2[8]
"""
dist_palp = self.line_width # Palpation distance to seek for a wall
mrot = np.array([[0, -1], [1, 0]]) # Rotation matrix for a quarter turn
for i in range(len(orig_seq)):
ibeg = i # Index of the first point of the segment
iend = i + 1 # Index of the last point of the segment
if iend == len(orig_seq):
iend = i - 1
xperp = np.dot(mrot, orig_seq[iend] - orig_seq[ibeg])
xperp = xperp / np.sqrt((xperp ** 2).sum(-1))
testleft = orig_seq[ibeg] + xperp * dist_palp
materialleft = False # Is there already extruded material at the left of the segment
testright = orig_seq[ibeg] - xperp * dist_palp
materialright = False # Is there already extruded material at the right of the segment
if self.vd1.shape[0]:
relpos = np.clip(((testleft - self.vd1) * (self.vd2 - self.vd1)).sum(1)
/ ((self.vd2 - self.vd1) * (self.vd2 - self.vd1)).sum(1), 0., 1.)
nearpoints = self.vd1 + relpos[:, np.newaxis] * (self.vd2 - self.vd1)
# nearpoints is the array of the nearest points of each segment
# from the point testleft
dist = ((testleft - nearpoints) * (testleft - nearpoints)).sum(1)
# dist is the array of the squares of the distances between testleft
# and each segment
if np.amin(dist) <= dist_palp * dist_palp:
materialleft = True
# Now the same computation with the point testright at the other side of the
# current segment
relpos = np.clip(((testright - self.vd1) * (self.vd2 - self.vd1)).sum(1)
/ ((self.vd2 - self.vd1) * (self.vd2 - self.vd1)).sum(1), 0., 1.)
nearpoints = self.vd1 + relpos[:, np.newaxis] * (self.vd2 - self.vd1)
dist = ((testright - nearpoints) * (testright - nearpoints)).sum(1)
if np.amin(dist) <= dist_palp * dist_palp:
materialright = True
if materialleft and not materialright:
modif_seq[ibeg] = modif_seq[ibeg] + xperp * self.pw_stretch
elif not materialleft and materialright:
modif_seq[ibeg] = modif_seq[ibeg] - xperp * self.pw_stretch
if materialleft and materialright:
modif_seq[ibeg] = orig_seq[ibeg] # Surrounded by walls, don't move
# Setup part of the stretch plugin
class Stretch(Script):
"""
Setup part of the stretch algorithm
The only parameter is the stretch distance
"""
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name":"Post stretch script",
"key": "Stretch",
"metadata": {},
"version": 2,
"settings":
{
"wc_stretch":
{
"label": "Wide circle stretch distance",
"description": "Distance by which the points are moved by the correction effect in corners. The higher this value, the higher the effect",
"unit": "mm",
"type": "float",
"default_value": 0.08,
"minimum_value": 0,
"minimum_value_warning": 0,
"maximum_value_warning": 0.2
},
"pw_stretch":
{
"label": "Push Wall stretch distance",
"description": "Distance by which the points are moved by the correction effect when two lines are nearby. The higher this value, the higher the effect",
"unit": "mm",
"type": "float",
"default_value": 0.08,
"minimum_value": 0,
"minimum_value_warning": 0,
"maximum_value_warning": 0.2
}
}
}"""
def execute(self, data):
"""
Entry point of the plugin.
data is the list of original g-code instructions,
the returned string is the list of modified g-code instructions
"""
stretcher = Stretcher(
Application.getInstance().getGlobalContainerStack().getProperty("line_width", "value")
, self.getSettingValueByKey("wc_stretch"), self.getSettingValueByKey("pw_stretch"))
return stretcher.execute(data)

View file

@ -0,0 +1,495 @@
# TweakAtZ script - Change printing parameters at a given height
# This script is the successor of the TweakAtZ plugin for legacy Cura.
# It contains code from the TweakAtZ plugin V1.0-V4.x and from the ExampleScript by Jaime van Kessel, Ultimaker B.V.
# It runs with the PostProcessingPlugin which is released under the terms of the AGPLv3 or higher.
# This script is licensed under the Creative Commons - Attribution - Share Alike (CC BY-SA) terms
#Authors of the TweakAtZ plugin / script:
# Written by Steven Morlock, smorloc@gmail.com
# Modified by Ricardo Gomez, ricardoga@otulook.com, to add Bed Temperature and make it work with Cura_13.06.04+
# Modified by Stefan Heule, Dim3nsioneer@gmx.ch since V3.0 (see changelog below)
# Modified by Jaime van Kessel (Ultimaker), j.vankessel@ultimaker.com to make it work for 15.10 / 2.x
# Modified by Ruben Dulek (Ultimaker), r.dulek@ultimaker.com, to debug.
##history / changelog:
##V3.0.1: TweakAtZ-state default 1 (i.e. the plugin works without any TweakAtZ comment)
##V3.1: Recognizes UltiGCode and deactivates value reset, fan speed added, alternatively layer no. to tweak at,
## extruder three temperature disabled by "#Ex3"
##V3.1.1: Bugfix reset flow rate
##V3.1.2: Bugfix disable TweakAtZ on Cool Head Lift
##V3.2: Flow rate for specific extruder added (only for 2 extruders), bugfix parser,
## added speed reset at the end of the print
##V4.0: Progress bar, tweaking over multiple layers, M605&M606 implemented, reset after one layer option,
## extruder three code removed, tweaking print speed, save call of Publisher class,
## uses previous value from other plugins also on UltiGCode
##V4.0.1: Bugfix for doubled G1 commands
##V4.0.2: uses Cura progress bar instead of its own
##V4.0.3: Bugfix for cool head lift (contributed by luisonoff)
##V4.9.91: First version for Cura 15.06.x and PostProcessingPlugin
##V4.9.92: Modifications for Cura 15.10
##V4.9.93: Minor bugfixes (input settings) / documentation
##V4.9.94: Bugfix Combobox-selection; remove logger
##V5.0: Bugfix for fall back after one layer and doubled G0 commands when using print speed tweak, Initial version for Cura 2.x
##V5.0.1: Bugfix for calling unknown property 'bedTemp' of previous settings storage and unkown variable 'speed'
##V5.1: API Changes included for use with Cura 2.2
## Uses -
## M220 S<factor in percent> - set speed factor override percentage
## M221 S<factor in percent> - set flow factor override percentage
## M221 S<factor in percent> T<0-#toolheads> - set flow factor override percentage for single extruder
## M104 S<temp> T<0-#toolheads> - set extruder <T> to target temperature <S>
## M140 S<temp> - set bed target temperature
## M106 S<PWM> - set fan speed to target speed <S>
## M605/606 to save and recall material settings on the UM2
from ..Script import Script
#from UM.Logger import Logger
import re
class TweakAtZ(Script):
version = "5.1.1"
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name":"ChangeAtZ """ + self.version + """ (Experimental)",
"key":"TweakAtZ",
"metadata": {},
"version": 2,
"settings":
{
"a_trigger":
{
"label": "Trigger",
"description": "Trigger at height or at layer no.",
"type": "enum",
"options": {"height":"Height","layer_no":"Layer No."},
"default_value": "height"
},
"b_targetZ":
{
"label": "Change Height",
"description": "Z height to change at",
"unit": "mm",
"type": "float",
"default_value": 5.0,
"minimum_value": "0",
"minimum_value_warning": "0.1",
"maximum_value_warning": "230",
"enabled": "a_trigger == 'height'"
},
"b_targetL":
{
"label": "Change Layer",
"description": "Layer no. to change at",
"unit": "",
"type": "int",
"default_value": 1,
"minimum_value": "-100",
"minimum_value_warning": "-1",
"enabled": "a_trigger == 'layer_no'"
},
"c_behavior":
{
"label": "Behavior",
"description": "Select behavior: Change value and keep it for the rest, Change value for single layer only",
"type": "enum",
"options": {"keep_value":"Keep value","single_layer":"Single Layer"},
"default_value": "keep_value"
},
"d_twLayers":
{
"label": "No. Layers",
"description": "No. of layers used to change",
"unit": "",
"type": "int",
"default_value": 1,
"minimum_value": "1",
"maximum_value_warning": "50",
"enabled": "c_behavior == 'keep_value'"
},
"e1_Tweak_speed":
{
"label": "Change Speed",
"description": "Select if total speed (print and travel) has to be cahnged",
"type": "bool",
"default_value": false
},
"e2_speed":
{
"label": "Speed",
"description": "New total speed (print and travel)",
"unit": "%",
"type": "int",
"default_value": 100,
"minimum_value": "1",
"minimum_value_warning": "10",
"maximum_value_warning": "200",
"enabled": "e1_Tweak_speed"
},
"f1_Tweak_printspeed":
{
"label": "Change Print Speed",
"description": "Select if print speed has to be changed",
"type": "bool",
"default_value": false
},
"f2_printspeed":
{
"label": "Print Speed",
"description": "New print speed",
"unit": "%",
"type": "int",
"default_value": 100,
"minimum_value": "1",
"minimum_value_warning": "10",
"maximum_value_warning": "200",
"enabled": "f1_Tweak_printspeed"
},
"g1_Tweak_flowrate":
{
"label": "Change Flow Rate",
"description": "Select if flow rate has to be changed",
"type": "bool",
"default_value": false
},
"g2_flowrate":
{
"label": "Flow Rate",
"description": "New Flow rate",
"unit": "%",
"type": "int",
"default_value": 100,
"minimum_value": "1",
"minimum_value_warning": "10",
"maximum_value_warning": "200",
"enabled": "g1_Tweak_flowrate"
},
"g3_Tweak_flowrateOne":
{
"label": "Change Flow Rate 1",
"description": "Select if first extruder flow rate has to be changed",
"type": "bool",
"default_value": false
},
"g4_flowrateOne":
{
"label": "Flow Rate One",
"description": "New Flow rate Extruder 1",
"unit": "%",
"type": "int",
"default_value": 100,
"minimum_value": "1",
"minimum_value_warning": "10",
"maximum_value_warning": "200",
"enabled": "g3_Tweak_flowrateOne"
},
"g5_Tweak_flowrateTwo":
{
"label": "Change Flow Rate 2",
"description": "Select if second extruder flow rate has to be changed",
"type": "bool",
"default_value": false
},
"g6_flowrateTwo":
{
"label": "Flow Rate two",
"description": "New Flow rate Extruder 2",
"unit": "%",
"type": "int",
"default_value": 100,
"minimum_value": "1",
"minimum_value_warning": "10",
"maximum_value_warning": "200",
"enabled": "g5_Tweak_flowrateTwo"
},
"h1_Tweak_bedTemp":
{
"label": "Change Bed Temp",
"description": "Select if Bed Temperature has to be changed",
"type": "bool",
"default_value": false
},
"h2_bedTemp":
{
"label": "Bed Temp",
"description": "New Bed Temperature",
"unit": "C",
"type": "float",
"default_value": 60,
"minimum_value": "0",
"minimum_value_warning": "30",
"maximum_value_warning": "120",
"enabled": "h1_Tweak_bedTemp"
},
"i1_Tweak_extruderOne":
{
"label": "Change Extruder 1 Temp",
"description": "Select if First Extruder Temperature has to be changed",
"type": "bool",
"default_value": false
},
"i2_extruderOne":
{
"label": "Extruder 1 Temp",
"description": "New First Extruder Temperature",
"unit": "C",
"type": "float",
"default_value": 190,
"minimum_value": "0",
"minimum_value_warning": "160",
"maximum_value_warning": "250",
"enabled": "i1_Tweak_extruderOne"
},
"i3_Tweak_extruderTwo":
{
"label": "Change Extruder 2 Temp",
"description": "Select if Second Extruder Temperature has to be changed",
"type": "bool",
"default_value": false
},
"i4_extruderTwo":
{
"label": "Extruder 2 Temp",
"description": "New Second Extruder Temperature",
"unit": "C",
"type": "float",
"default_value": 190,
"minimum_value": "0",
"minimum_value_warning": "160",
"maximum_value_warning": "250",
"enabled": "i3_Tweak_extruderTwo"
},
"j1_Tweak_fanSpeed":
{
"label": "Change Fan Speed",
"description": "Select if Fan Speed has to be changed",
"type": "bool",
"default_value": false
},
"j2_fanSpeed":
{
"label": "Fan Speed",
"description": "New Fan Speed (0-255)",
"unit": "PWM",
"type": "int",
"default_value": 255,
"minimum_value": "0",
"minimum_value_warning": "15",
"maximum_value_warning": "255",
"enabled": "j1_Tweak_fanSpeed"
}
}
}"""
def getValue(self, line, key, default = None): #replace default getvalue due to comment-reading feature
if not key in line or (";" in line and line.find(key) > line.find(";") and
not ";TweakAtZ" in key and not ";LAYER:" in key):
return default
subPart = line[line.find(key) + len(key):] #allows for string lengths larger than 1
if ";TweakAtZ" in key:
m = re.search("^[0-4]", subPart)
elif ";LAYER:" in key:
m = re.search("^[+-]?[0-9]*", subPart)
else:
#the minus at the beginning allows for negative values, e.g. for delta printers
m = re.search("^[-]?[0-9]*\.?[0-9]*", subPart)
if m == None:
return default
try:
return float(m.group(0))
except:
return default
def execute(self, data):
#Check which tweaks should apply
TweakProp = {"speed": self.getSettingValueByKey("e1_Tweak_speed"),
"flowrate": self.getSettingValueByKey("g1_Tweak_flowrate"),
"flowrateOne": self.getSettingValueByKey("g3_Tweak_flowrateOne"),
"flowrateTwo": self.getSettingValueByKey("g5_Tweak_flowrateTwo"),
"bedTemp": self.getSettingValueByKey("h1_Tweak_bedTemp"),
"extruderOne": self.getSettingValueByKey("i1_Tweak_extruderOne"),
"extruderTwo": self.getSettingValueByKey("i3_Tweak_extruderTwo"),
"fanSpeed": self.getSettingValueByKey("j1_Tweak_fanSpeed")}
TweakPrintSpeed = self.getSettingValueByKey("f1_Tweak_printspeed")
TweakStrings = {"speed": "M220 S%f\n",
"flowrate": "M221 S%f\n",
"flowrateOne": "M221 T0 S%f\n",
"flowrateTwo": "M221 T1 S%f\n",
"bedTemp": "M140 S%f\n",
"extruderOne": "M104 S%f T0\n",
"extruderTwo": "M104 S%f T1\n",
"fanSpeed": "M106 S%d\n"}
target_values = {"speed": self.getSettingValueByKey("e2_speed"),
"printspeed": self.getSettingValueByKey("f2_printspeed"),
"flowrate": self.getSettingValueByKey("g2_flowrate"),
"flowrateOne": self.getSettingValueByKey("g4_flowrateOne"),
"flowrateTwo": self.getSettingValueByKey("g6_flowrateTwo"),
"bedTemp": self.getSettingValueByKey("h2_bedTemp"),
"extruderOne": self.getSettingValueByKey("i2_extruderOne"),
"extruderTwo": self.getSettingValueByKey("i4_extruderTwo"),
"fanSpeed": self.getSettingValueByKey("j2_fanSpeed")}
old = {"speed": -1, "flowrate": -1, "flowrateOne": -1, "flowrateTwo": -1, "platformTemp": -1, "extruderOne": -1,
"extruderTwo": -1, "bedTemp": -1, "fanSpeed": -1, "state": -1}
twLayers = self.getSettingValueByKey("d_twLayers")
if self.getSettingValueByKey("c_behavior") == "single_layer":
behavior = 1
else:
behavior = 0
try:
twLayers = max(int(twLayers),1) #for the case someone entered something as "funny" as -1
except:
twLayers = 1
pres_ext = 0
done_layers = 0
z = 0
x = None
y = None
layer = -100000 #layer no. may be negative (raft) but never that low
# state 0: deactivated, state 1: activated, state 2: active, but below z,
# state 3: active and partially executed (multi layer), state 4: active and passed z
state = 1
# IsUM2: Used for reset of values (ok for Marlin/Sprinter),
# has to be set to 1 for UltiGCode (work-around for missing default values)
IsUM2 = False
oldValueUnknown = False
TWinstances = 0
if self.getSettingValueByKey("a_trigger") == "layer_no":
targetL_i = int(self.getSettingValueByKey("b_targetL"))
targetZ = 100000
else:
targetL_i = -100000
targetZ = self.getSettingValueByKey("b_targetZ")
index = 0
for active_layer in data:
modified_gcode = ""
lines = active_layer.split("\n")
for line in lines:
if ";Generated with Cura_SteamEngine" in line:
TWinstances += 1
modified_gcode += ";TweakAtZ instances: %d\n" % TWinstances
if not ("M84" in line or "M25" in line or ("G1" in line and TweakPrintSpeed and (state==3 or state==4)) or
";TweakAtZ instances:" in line):
modified_gcode += line + "\n"
IsUM2 = ("FLAVOR:UltiGCode" in line) or IsUM2 #Flavor is UltiGCode!
if ";TweakAtZ-state" in line: #checks for state change comment
state = self.getValue(line, ";TweakAtZ-state", state)
if ";TweakAtZ instances:" in line:
try:
tempTWi = int(line[20:])
except:
tempTWi = TWinstances
TWinstances = tempTWi
if ";Small layer" in line: #checks for begin of Cool Head Lift
old["state"] = state
state = 0
if ";LAYER:" in line: #new layer no. found
if state == 0:
state = old["state"]
layer = self.getValue(line, ";LAYER:", layer)
if targetL_i > -100000: #target selected by layer no.
if (state == 2 or targetL_i == 0) and layer == targetL_i: #determine targetZ from layer no.; checks for tweak on layer 0
state = 2
targetZ = z + 0.001
if (self.getValue(line, "T", None) is not None) and (self.getValue(line, "M", None) is None): #looking for single T-cmd
pres_ext = self.getValue(line, "T", pres_ext)
if "M190" in line or "M140" in line and state < 3: #looking for bed temp, stops after target z is passed
old["bedTemp"] = self.getValue(line, "S", old["bedTemp"])
if "M109" in line or "M104" in line and state < 3: #looking for extruder temp, stops after target z is passed
if self.getValue(line, "T", pres_ext) == 0:
old["extruderOne"] = self.getValue(line, "S", old["extruderOne"])
elif self.getValue(line, "T", pres_ext) == 1:
old["extruderTwo"] = self.getValue(line, "S", old["extruderTwo"])
if "M107" in line: #fan is stopped; is always updated in order not to miss switch off for next object
old["fanSpeed"] = 0
if "M106" in line and state < 3: #looking for fan speed
old["fanSpeed"] = self.getValue(line, "S", old["fanSpeed"])
if "M221" in line and state < 3: #looking for flow rate
tmp_extruder = self.getValue(line,"T",None)
if tmp_extruder == None: #check if extruder is specified
old["flowrate"] = self.getValue(line, "S", old["flowrate"])
elif tmp_extruder == 0: #first extruder
old["flowrateOne"] = self.getValue(line, "S", old["flowrateOne"])
elif tmp_extruder == 1: #second extruder
old["flowrateOne"] = self.getValue(line, "S", old["flowrateOne"])
if ("M84" in line or "M25" in line):
if state>0 and TweakProp["speed"]: #"finish" commands for UM Original and UM2
modified_gcode += "M220 S100 ; speed reset to 100% at the end of print\n"
modified_gcode += "M117 \n"
modified_gcode += line + "\n"
if "G1" in line or "G0" in line:
newZ = self.getValue(line, "Z", z)
x = self.getValue(line, "X", None)
y = self.getValue(line, "Y", None)
e = self.getValue(line, "E", None)
f = self.getValue(line, "F", None)
if 'G1' in line and TweakPrintSpeed and (state==3 or state==4):
# check for pure print movement in target range:
if x != None and y != None and f != None and e != None and newZ==z:
modified_gcode += "G1 F%d X%1.3f Y%1.3f E%1.5f\n" % (int(f / 100.0 * float(target_values["printspeed"])), self.getValue(line, "X"),
self.getValue(line, "Y"), self.getValue(line, "E"))
else: #G1 command but not a print movement
modified_gcode += line + "\n"
# no tweaking on retraction hops which have no x and y coordinate:
if (newZ != z) and (x is not None) and (y is not None):
z = newZ
if z < targetZ and state == 1:
state = 2
if z >= targetZ and state == 2:
state = 3
done_layers = 0
for key in TweakProp:
if TweakProp[key] and old[key]==-1: #old value is not known
oldValueUnknown = True
if oldValueUnknown: #the tweaking has to happen within one layer
twLayers = 1
if IsUM2: #Parameters have to be stored in the printer (UltiGCode=UM2)
modified_gcode += "M605 S%d;stores parameters before tweaking\n" % (TWinstances-1)
if behavior == 1: #single layer tweak only and then reset
twLayers = 1
if TweakPrintSpeed and behavior == 0:
twLayers = done_layers + 1
if state==3:
if twLayers-done_layers>0: #still layers to go?
if targetL_i > -100000:
modified_gcode += ";TweakAtZ V%s: executed at Layer %d\n" % (self.version,layer)
modified_gcode += "M117 Printing... tw@L%4d\n" % layer
else:
modified_gcode += (";TweakAtZ V%s: executed at %1.2f mm\n" % (self.version,z))
modified_gcode += "M117 Printing... tw@%5.1f\n" % z
for key in TweakProp:
if TweakProp[key]:
modified_gcode += TweakStrings[key] % float(old[key]+(float(target_values[key])-float(old[key]))/float(twLayers)*float(done_layers+1))
done_layers += 1
else:
state = 4
if behavior == 1: #reset values after one layer
if targetL_i > -100000:
modified_gcode += ";TweakAtZ V%s: reset on Layer %d\n" % (self.version,layer)
else:
modified_gcode += ";TweakAtZ V%s: reset at %1.2f mm\n" % (self.version,z)
if IsUM2 and oldValueUnknown: #executes on UM2 with Ultigcode and machine setting
modified_gcode += "M606 S%d;recalls saved settings\n" % (TWinstances-1)
else: #executes on RepRap, UM2 with Ultigcode and Cura setting
for key in TweakProp:
if TweakProp[key]:
modified_gcode += TweakStrings[key] % float(old[key])
# re-activates the plugin if executed by pre-print G-command, resets settings:
if (z < targetZ or layer == 0) and state >= 3: #resets if below tweak level or at level 0
state = 2
done_layers = 0
if targetL_i > -100000:
modified_gcode += ";TweakAtZ V%s: reset below Layer %d\n" % (self.version,targetL_i)
else:
modified_gcode += ";TweakAtZ V%s: reset below %1.2f mm\n" % (self.version,targetZ)
if IsUM2 and oldValueUnknown: #executes on UM2 with Ultigcode and machine setting
modified_gcode += "M606 S%d;recalls saved settings\n" % (TWinstances-1)
else: #executes on RepRap, UM2 with Ultigcode and Cura setting
for key in TweakProp:
if TweakProp[key]:
modified_gcode += TweakStrings[key] % float(old[key])
data[index] = modified_gcode
index += 1
return data

View file

@ -106,7 +106,7 @@ class SimulationPass(RenderPass):
nozzle_node = node
nozzle_node.setVisible(False)
elif issubclass(type(node), SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible() and node.callDecoration("getBuildPlateNumber") == active_build_plate:
elif isinstance(node, SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible():
layer_data = node.callDecoration("getLayerData")
if not layer_data:
continue

View file

@ -1,4 +1,4 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import sys
@ -98,13 +98,16 @@ class SimulationView(View):
self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count"))
self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers"))
self._compatibility_mode = True # for safety
self._compatibility_mode = self._evaluateCompatibilityMode()
self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"),
title = catalog.i18nc("@info:title", "Simulation View"))
def _evaluateCompatibilityMode(self):
return OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode"))
def _resetSettings(self):
self._layer_view_type = 0 # 0 is material color, 1 is color by linetype, 2 is speed
self._layer_view_type = 0 # 0 is material color, 1 is color by linetype, 2 is speed, 3 is layer thickness
self._extruder_count = 0
self._extruder_opacity = [1.0, 1.0, 1.0, 1.0]
self._show_travel_moves = 0
@ -127,7 +130,7 @@ class SimulationView(View):
# Currently the RenderPass constructor requires a size > 0
# This should be fixed in RenderPass's constructor.
self._layer_pass = SimulationPass(1, 1)
self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode"))
self._compatibility_mode = self._evaluateCompatibilityMode()
self._layer_pass.setSimulationView(self)
return self._layer_pass
@ -344,7 +347,12 @@ class SimulationView(View):
self._max_feedrate = max(float(p.lineFeedrates.max()), self._max_feedrate)
self._min_feedrate = min(float(p.lineFeedrates.min()), self._min_feedrate)
self._max_thickness = max(float(p.lineThicknesses.max()), self._max_thickness)
self._min_thickness = min(float(p.lineThicknesses.min()), self._min_thickness)
try:
self._min_thickness = min(float(p.lineThicknesses[numpy.nonzero(p.lineThicknesses)].min()), self._min_thickness)
except:
# 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")
if max_layer_number < layer_id:
max_layer_number = layer_id
if min_layer_number > layer_id:
@ -529,8 +537,7 @@ class SimulationView(View):
def _updateWithPreferences(self):
self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count"))
self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers"))
self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool(
Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode"))
self._compatibility_mode = self._evaluateCompatibilityMode()
self.setSimulationViewType(int(float(Preferences.getInstance().getValue("layerview/layer_view_type"))));

View file

@ -176,7 +176,6 @@ Item
viewSettings.show_feedrate_gradient = viewSettings.show_gradient && (type_id == 2);
viewSettings.show_thickness_gradient = viewSettings.show_gradient && (type_id == 3);
}
}
Label
@ -486,7 +485,7 @@ Item
}
}
// Gradient colors for layer thickness
// Gradient colors for layer thickness (similar to parula colormap)
Rectangle { // In QML 5.9 can be changed by LinearGradient
// Invert values because then the bar is rotated 90 degrees
id: thicknessGradient
@ -500,23 +499,23 @@ Item
gradient: Gradient {
GradientStop {
position: 0.000
color: Qt.rgba(1, 0, 0, 1)
color: Qt.rgba(1, 1, 0, 1)
}
GradientStop {
position: 0.25
color: Qt.rgba(0.5, 0.5, 0, 1)
color: Qt.rgba(1, 0.75, 0.25, 1)
}
GradientStop {
position: 0.5
color: Qt.rgba(0, 1, 0, 1)
color: Qt.rgba(0, 0.75, 0.5, 1)
}
GradientStop {
position: 0.75
color: Qt.rgba(0, 0.5, 0.5, 1)
color: Qt.rgba(0, 0.375, 0.75, 1)
}
GradientStop {
position: 1.0
color: Qt.rgba(0, 0, 1, 1)
color: Qt.rgba(0, 0, 0.5, 1)
}
}
}

View file

@ -1,4 +1,4 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty
@ -117,7 +117,7 @@ class SimulationViewProxy(QObject):
def setSimulationViewType(self, layer_view_type):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setSimulationViewisinstance(layer_view_type)
active_view.setSimulationViewType(layer_view_type)
@pyqtSlot(result=int)
def getSimulationViewType(self):

View file

@ -1,4 +1,4 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtQml import qmlRegisterSingletonType
@ -18,7 +18,7 @@ def getMetaData():
}
def createSimulationViewProxy(engine, script_engine):
return SimulationViewProxy.SimulatorViewProxy()
return SimulationViewProxy.SimulationViewProxy()
def register(app):
simulation_view = SimulationView.SimulationView()

View file

@ -54,9 +54,13 @@ vertex41core =
vec4 layerThicknessGradientColor(float abs_value, float min_value, float max_value)
{
float value = (abs_value - min_value)/(max_value - min_value);
float red = max(2*value-1, 0);
float green = 1-abs(1-2*value);
float blue = max(1-2*value, 0);
float red = min(max(4*value-2, 0), 1);
float green = min(1.5*value, 0.75);
if (value > 0.75)
{
green = value;
}
float blue = 0.75-abs(0.25-value);
return vec4(red, green, blue, 1.0);
}

View file

@ -145,35 +145,42 @@ geometry41core =
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head + g_vertex_offset_vert));
//And reverse so that the line is also visible from the back side.
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
EndPrimitive();
} else {
// All normal lines are rendered as 3d tubes.
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz));
EndPrimitive();
// left side
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
EndPrimitive();
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
EndPrimitive();

View file

@ -107,7 +107,7 @@ class SliceInfo(Extension):
"brand": extruder.material.getMetaData().get("brand", "")
}
extruder_position = int(extruder.getMetaDataEntry("position", "0"))
if extruder_position in print_information.materialLengths:
if len(print_information.materialLengths) > extruder_position:
extruder_dict["material_used"] = print_information.materialLengths[extruder_position]
extruder_dict["variant"] = extruder.variant.getName()
extruder_dict["nozzle_size"] = extruder.getProperty("machine_nozzle_size", "value")

View file

@ -28,6 +28,7 @@ class SolidView(View):
self._enabled_shader = None
self._disabled_shader = None
self._non_printing_shader = None
self._support_mesh_shader = None
self._extruders_model = ExtrudersModel()
self._theme = None
@ -54,6 +55,11 @@ class SolidView(View):
self._non_printing_shader.setUniformValue("u_diffuseColor", Color(*self._theme.getColor("model_non_printing").getRgb()))
self._non_printing_shader.setUniformValue("u_opacity", 0.6)
if not self._support_mesh_shader:
self._support_mesh_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "striped.shader"))
self._support_mesh_shader.setUniformValue("u_vertical_stripes", True)
self._support_mesh_shader.setUniformValue("u_width", 5.0)
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
support_extruder_nr = global_container_stack.getProperty("support_extruder_nr", "value")
@ -117,6 +123,16 @@ class SolidView(View):
renderer.queueNode(node, shader = self._non_printing_shader, transparent = True)
elif getattr(node, "_outside_buildarea", False):
renderer.queueNode(node, shader = self._disabled_shader)
elif per_mesh_stack and per_mesh_stack.getProperty("support_mesh", "value"):
# Render support meshes with a vertical stripe that is darker
shade_factor = 0.6
uniforms["diffuse_color_2"] = [
uniforms["diffuse_color"][0] * shade_factor,
uniforms["diffuse_color"][1] * shade_factor,
uniforms["diffuse_color"][2] * shade_factor,
1.0
]
renderer.queueNode(node, shader = self._support_mesh_shader, uniforms = uniforms)
else:
renderer.queueNode(node, shader = self._enabled_shader, uniforms = uniforms)
if node.callDecoration("isGroup") and Selection.isSelected(node):

View file

@ -0,0 +1,68 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Tool import Tool
from PyQt5.QtCore import Qt, QUrl
from UM.Application import Application
from UM.Event import Event
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Settings.SettingInstance import SettingInstance
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
import os
import os.path
class SupportEraser(Tool):
def __init__(self):
super().__init__()
self._shortcut_key = Qt.Key_G
self._controller = Application.getInstance().getController()
def event(self, event):
super().event(event)
if event.type == Event.ToolActivateEvent:
# Load the remover mesh:
self._createEraserMesh()
# After we load the mesh, deactivate the tool again:
self.getController().setActiveTool(None)
def _createEraserMesh(self):
node = CuraSceneNode()
node.setName("Eraser")
node.setSelectable(True)
mesh = MeshBuilder()
mesh.addCube(10,10,10)
node.setMeshData(mesh.build())
active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
node.addDecorator(SettingOverrideDecorator())
node.addDecorator(BuildPlateDecorator(active_build_plate))
node.addDecorator(SliceableObjectDecorator())
stack = node.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
if not stack:
node.addDecorator(SettingOverrideDecorator())
stack = node.callDecoration("getStack")
settings = stack.getTop()
if not (settings.getInstance("anti_overhang_mesh") and settings.getProperty("anti_overhang_mesh", "value")):
definition = stack.getSettingDefinition("anti_overhang_mesh")
new_instance = SettingInstance(definition, settings)
new_instance.setProperty("value", True)
new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance)
scene = self._controller.getScene()
op = AddSceneNodeOperation(node, scene.getRoot())
op.push()
Application.getInstance().getController().getScene().sceneChanged.emit(node)

View file

@ -0,0 +1,20 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import SupportEraser
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("uranium")
def getMetaData():
return {
"tool": {
"name": i18n_catalog.i18nc("@label", "Support Blocker"),
"description": i18n_catalog.i18nc("@info:tooltip", "Create a volume in which supports are not printed."),
"icon": "tool_icon.svg",
"weight": 4
}
}
def register(app):
return { "tool": SupportEraser.SupportEraser() }

View file

@ -0,0 +1,8 @@
{
"name": "Support Eraser",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Creates an eraser mesh to block the printing of support in certain places",
"api": 4,
"i18n-catalog": "cura"
}

View file

@ -0,0 +1,11 @@
<svg width="30px" height="30px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Cura-Icon" fill="#000000">
<path d="M19,27 L12,27 L3,27 L3,3 L12,3 L12,11 L19,11 L19,19 L27,19 L27,27 L19,27 Z M4,4 L4,26 L11,26 L11,4 L4,4 Z" id="Combined-Shape"></path>
<polygon id="Path" points="10 17.1441441 9.18918919 17.954955 7.52252252 16.3333333 5.85585586 18 5.04504505 17.1891892 6.66666667 15.4774775 5 13.8558559 5.81081081 13.045045 7.52252252 14.6666667 9.18918919 13 10 13.8108108 8.33333333 15.4774775"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 866 B

View file

@ -0,0 +1,56 @@
#Copyright (c) 2018 Ultimaker B.V.
#Cura is released under the terms of the LGPLv3 or higher.
from Charon.VirtualFile import VirtualFile #To open UFP files.
from Charon.OpenMode import OpenMode #To indicate that we want to write to UFP files.
from io import StringIO #For converting g-code to bytes.
from UM.Application import Application
from UM.Logger import Logger
from UM.Mesh.MeshWriter import MeshWriter #The writer we need to implement.
from UM.PluginRegistry import PluginRegistry #To get the g-code writer.
from PyQt5.QtCore import QBuffer
from cura.Snapshot import Snapshot
class UFPWriter(MeshWriter):
def __init__(self):
super().__init__()
self._snapshot = None
Application.getInstance().getOutputDeviceManager().writeStarted.connect(self._createSnapshot)
def _createSnapshot(self, *args):
# must be called from the main thread because of OpenGL
Logger.log("d", "Creating thumbnail image...")
self._snapshot = Snapshot.snapshot(width = 300, height = 300)
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode):
archive = VirtualFile()
archive.openStream(stream, "application/x-ufp", OpenMode.WriteOnly)
#Store the g-code from the scene.
archive.addContentType(extension = "gcode", mime_type = "text/x-gcode")
gcode_textio = StringIO() #We have to convert the g-code into bytes.
PluginRegistry.getInstance().getPluginObject("GCodeWriter").write(gcode_textio, None)
gcode = archive.getStream("/3D/model.gcode")
gcode.write(gcode_textio.getvalue().encode("UTF-8"))
archive.addRelation(virtual_path = "/3D/model.gcode", relation_type = "http://schemas.ultimaker.org/package/2018/relationships/gcode")
#Store the thumbnail.
if self._snapshot:
archive.addContentType(extension = "png", mime_type = "image/png")
thumbnail = archive.getStream("/Metadata/thumbnail.png")
thumbnail_buffer = QBuffer()
thumbnail_buffer.open(QBuffer.ReadWrite)
thumbnail_image = self._snapshot
thumbnail_image.save(thumbnail_buffer, "PNG")
thumbnail.write(thumbnail_buffer.data())
archive.addRelation(virtual_path = "/Metadata/thumbnail.png", relation_type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail", origin = "/3D/model.gcode")
else:
Logger.log("d", "Thumbnail not created, cannot save it")
archive.close()
return True

View file

@ -0,0 +1,38 @@
#Copyright (c) 2018 Ultimaker B.V.
#Cura is released under the terms of the LGPLv3 or higher.
import sys
from UM.Logger import Logger
try:
from . import UFPWriter
except ImportError:
Logger.log("w", "Could not import UFPWriter; libCharon may be missing")
from UM.i18n import i18nCatalog #To translate the file format description.
from UM.Mesh.MeshWriter import MeshWriter #For the binary mode flag.
i18n_catalog = i18nCatalog("cura")
def getMetaData():
if "UFPWriter.UFPWriter" not in sys.modules:
return {}
return {
"mesh_writer": {
"output": [
{
"mime_type": "application/x-ufp",
"mode": MeshWriter.OutputMode.BinaryMode,
"extension": "ufp",
"description": i18n_catalog.i18nc("@item:inlistbox", "Ultimaker Format Package")
}
]
}
}
def register(app):
if "UFPWriter.UFPWriter" not in sys.modules:
return {}
return { "mesh_writer": UFPWriter.UFPWriter() }

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