mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-23 14:44:13 -06:00
Merge branch 'master' into feature_setting_visibility_profiles
This commit is contained in:
commit
69d7bf4130
414 changed files with 40051 additions and 22967 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -47,7 +47,6 @@ plugins/Doodle3D-cura-plugin
|
|||
plugins/FlatProfileExporter
|
||||
plugins/GodMode
|
||||
plugins/OctoPrintPlugin
|
||||
plugins/PostProcessingPlugin
|
||||
plugins/ProfileFlattener
|
||||
plugins/X3GWriter
|
||||
|
||||
|
|
2
Jenkinsfile
vendored
2
Jenkinsfile
vendored
|
@ -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.
|
||||
|
|
20
README.md
20
README.md
|
@ -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
|
||||
----------------
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
70
cura/PrinterOutput/ExtruderOuputModel.py
Normal file
70
cura/PrinterOutput/ExtruderOuputModel.py
Normal 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()
|
34
cura/PrinterOutput/MaterialOutputModel.py
Normal file
34
cura/PrinterOutput/MaterialOutputModel.py
Normal 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
|
119
cura/PrinterOutput/NetworkCamera.py
Normal file
119
cura/PrinterOutput/NetworkCamera.py
Normal 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()
|
304
cura/PrinterOutput/NetworkedPrinterOutputDevice.py
Normal file
304
cura/PrinterOutput/NetworkedPrinterOutputDevice.py
Normal 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
|
101
cura/PrinterOutput/PrintJobOutputModel.py
Normal file
101
cura/PrinterOutput/PrintJobOutputModel.py
Normal 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)
|
46
cura/PrinterOutput/PrinterOutputController.py
Normal file
46
cura/PrinterOutput/PrinterOutputController.py
Normal 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")
|
240
cura/PrinterOutput/PrinterOutputModel.py
Normal file
240
cura/PrinterOutput/PrinterOutputModel.py
Normal 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
|
0
cura/PrinterOutput/__init__.py
Normal file
0
cura/PrinterOutput/__init__.py
Normal 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):
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"]:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 = ""
|
||||
|
|
|
@ -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.
|
|
@ -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:
|
||||
|
|
|
@ -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
115
cura/Snapshot.py
Normal 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
|
51
cura_app.py
51
cura_app.py
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -390,6 +390,13 @@ UM.Dialog
|
|||
}
|
||||
}
|
||||
|
||||
function accept() {
|
||||
manager.closeBackend();
|
||||
manager.onOkButtonClicked();
|
||||
base.visible = false;
|
||||
base.accept();
|
||||
}
|
||||
|
||||
function reject() {
|
||||
manager.onCancelButtonClicked();
|
||||
base.visible = false;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 model’s 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 system’s 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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 []
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"]]);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
474
plugins/PluginBrowser/PluginEntry.qml
Normal file
474
plugins/PluginBrowser/PluginEntry.qml
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
209
plugins/PostProcessingPlugin/PostProcessingPlugin.py
Normal file
209
plugins/PostProcessingPlugin/PostProcessingPlugin.py
Normal 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")
|
||||
|
||||
|
501
plugins/PostProcessingPlugin/PostProcessingPlugin.qml
Normal file
501
plugins/PostProcessingPlugin/PostProcessingPlugin.qml
Normal 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{ }
|
||||
}
|
||||
}
|
||||
}
|
2
plugins/PostProcessingPlugin/README.md
Normal file
2
plugins/PostProcessingPlugin/README.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
# PostProcessingPlugin
|
||||
A post processing plugin for Cura
|
111
plugins/PostProcessingPlugin/Script.py
Normal file
111
plugins/PostProcessingPlugin/Script.py
Normal 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()
|
11
plugins/PostProcessingPlugin/__init__.py
Normal file
11
plugins/PostProcessingPlugin/__init__.py
Normal 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()}
|
8
plugins/PostProcessingPlugin/plugin.json
Normal file
8
plugins/PostProcessingPlugin/plugin.json
Normal 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"
|
||||
}
|
47
plugins/PostProcessingPlugin/postprocessing.svg
Normal file
47
plugins/PostProcessingPlugin/postprocessing.svg
Normal 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 |
48
plugins/PostProcessingPlugin/scripts/BQ_PauseAtHeight.py
Normal file
48
plugins/PostProcessingPlugin/scripts/BQ_PauseAtHeight.py
Normal 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
|
76
plugins/PostProcessingPlugin/scripts/ColorChange.py
Normal file
76
plugins/PostProcessingPlugin/scripts/ColorChange.py
Normal 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
|
43
plugins/PostProcessingPlugin/scripts/ExampleScript.py
Normal file
43
plugins/PostProcessingPlugin/scripts/ExampleScript.py
Normal 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
|
221
plugins/PostProcessingPlugin/scripts/PauseAtHeight.py
Normal file
221
plugins/PostProcessingPlugin/scripts/PauseAtHeight.py
Normal 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
|
169
plugins/PostProcessingPlugin/scripts/PauseAtHeightforRepetier.py
Normal file
169
plugins/PostProcessingPlugin/scripts/PauseAtHeightforRepetier.py
Normal 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
|
56
plugins/PostProcessingPlugin/scripts/SearchAndReplace.py
Normal file
56
plugins/PostProcessingPlugin/scripts/SearchAndReplace.py
Normal 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
|
469
plugins/PostProcessingPlugin/scripts/Stretch.py
Normal file
469
plugins/PostProcessingPlugin/scripts/Stretch.py
Normal 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)
|
||||
|
495
plugins/PostProcessingPlugin/scripts/TweakAtZ.py
Normal file
495
plugins/PostProcessingPlugin/scripts/TweakAtZ.py
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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"))));
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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):
|
||||
|
|
68
plugins/SupportEraser/SupportEraser.py
Normal file
68
plugins/SupportEraser/SupportEraser.py
Normal 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)
|
20
plugins/SupportEraser/__init__.py
Normal file
20
plugins/SupportEraser/__init__.py
Normal 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() }
|
8
plugins/SupportEraser/plugin.json
Normal file
8
plugins/SupportEraser/plugin.json
Normal 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"
|
||||
}
|
11
plugins/SupportEraser/tool_icon.svg
Normal file
11
plugins/SupportEraser/tool_icon.svg
Normal 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 |
56
plugins/UFPWriter/UFPWriter.py
Normal file
56
plugins/UFPWriter/UFPWriter.py
Normal 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
|
38
plugins/UFPWriter/__init__.py
Normal file
38
plugins/UFPWriter/__init__.py
Normal 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
Loading…
Add table
Add a link
Reference in a new issue