mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-07 15:07:28 -06:00
Merge branch 'master' into feature_xmlmaterials_cura_settings
This commit is contained in:
commit
a85c42c246
617 changed files with 6736 additions and 2860 deletions
4
.dockerignore
Normal file
4
.dockerignore
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
resources/materials
|
||||||
|
CuraEngine
|
5
.github/ISSUE_TEMPLATE.md
vendored
5
.github/ISSUE_TEMPLATE.md
vendored
|
@ -6,7 +6,7 @@ Before filing, PLEASE check if the issue already exists (either open or closed)
|
||||||
Also, please note the application version in the title of the issue. For example: "[3.2.1] Cannot connect to 3rd-party printer". Please do not write thigns like "Request:" or "[BUG]" in the title; this is what labels are for.
|
Also, please note the application version in the title of the issue. For example: "[3.2.1] Cannot connect to 3rd-party printer". Please do not write thigns like "Request:" or "[BUG]" in the title; this is what labels are for.
|
||||||
|
|
||||||
It is also helpful to attach a project (.3mf or .curaproject) file and Cura log file so we can debug issues quicker.
|
It is also helpful to attach a project (.3mf or .curaproject) file and Cura log file so we can debug issues quicker.
|
||||||
Information about how to find the log file can be found at https://github.com/Ultimaker/Cura/wiki/Cura-Preferences-and-Settings-Locations. To upload a project, we recommend http://wetransfer.com, but other file hosts like Google Drive or Dropbox work well too.
|
Information about how to find the log file can be found at https://github.com/Ultimaker/Cura/wiki/Cura-Preferences-and-Settings-Locations. To upload a project, try changing the extension to e.g. .curaproject.3mf.zip so that github accepts uploading the file. Otherwise we recommend http://wetransfer.com, but other file hosts like Google Drive or Dropbox work well too.
|
||||||
|
|
||||||
Thank you for using Cura!
|
Thank you for using Cura!
|
||||||
-->
|
-->
|
||||||
|
@ -26,6 +26,9 @@ Thank you for using Cura!
|
||||||
**Display Driver**
|
**Display Driver**
|
||||||
<!-- Video driver name and version -->
|
<!-- Video driver name and version -->
|
||||||
|
|
||||||
|
**Printer**
|
||||||
|
<!-- Which printer was selected in Cura. Please attach project file as .curaproject.3mf.zip -->
|
||||||
|
|
||||||
**Steps to Reproduce**
|
**Steps to Reproduce**
|
||||||
<!-- Add the steps needed that lead up to the issue (replace this text) -->
|
<!-- Add the steps needed that lead up to the issue (replace this text) -->
|
||||||
|
|
||||||
|
|
45
Dockerfile
Normal file
45
Dockerfile
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
FROM ultimaker/cura-build-environment:1
|
||||||
|
|
||||||
|
# Environment vars for easy configuration
|
||||||
|
ENV CURA_APP_DIR=/srv/cura
|
||||||
|
|
||||||
|
# Ensure our sources dir exists
|
||||||
|
RUN mkdir $CURA_APP_DIR
|
||||||
|
|
||||||
|
# Setup CuraEngine
|
||||||
|
ENV CURA_ENGINE_BRANCH=master
|
||||||
|
WORKDIR $CURA_APP_DIR
|
||||||
|
RUN git clone -b $CURA_ENGINE_BRANCH --depth 1 https://github.com/Ultimaker/CuraEngine
|
||||||
|
WORKDIR $CURA_APP_DIR/CuraEngine
|
||||||
|
RUN mkdir build
|
||||||
|
WORKDIR $CURA_APP_DIR/CuraEngine/build
|
||||||
|
RUN cmake3 ..
|
||||||
|
RUN make
|
||||||
|
RUN make install
|
||||||
|
|
||||||
|
# TODO: setup libCharon
|
||||||
|
|
||||||
|
# Setup Uranium
|
||||||
|
ENV URANIUM_BRANCH=master
|
||||||
|
WORKDIR $CURA_APP_DIR
|
||||||
|
RUN git clone -b $URANIUM_BRANCH --depth 1 https://github.com/Ultimaker/Uranium
|
||||||
|
|
||||||
|
# Setup materials
|
||||||
|
ENV MATERIALS_BRANCH=master
|
||||||
|
WORKDIR $CURA_APP_DIR
|
||||||
|
RUN git clone -b $MATERIALS_BRANCH --depth 1 https://github.com/Ultimaker/fdm_materials materials
|
||||||
|
|
||||||
|
# Setup Cura
|
||||||
|
WORKDIR $CURA_APP_DIR/Cura
|
||||||
|
ADD . .
|
||||||
|
RUN mv $CURA_APP_DIR/materials resources/materials
|
||||||
|
|
||||||
|
# Make sure Cura can find CuraEngine
|
||||||
|
RUN ln -s /usr/local/bin/CuraEngine $CURA_APP_DIR/Cura
|
||||||
|
|
||||||
|
# Run Cura
|
||||||
|
WORKDIR $CURA_APP_DIR/Cura
|
||||||
|
ENV PYTHONPATH=${PYTHONPATH}:$CURA_APP_DIR/Uranium
|
||||||
|
RUN chmod +x ./CuraEngine
|
||||||
|
RUN chmod +x ./run_in_docker.sh
|
||||||
|
CMD "./run_in_docker.sh"
|
|
@ -111,6 +111,9 @@ class BuildVolume(SceneNode):
|
||||||
# but it does not update the disallowed areas after material change
|
# but it does not update the disallowed areas after material change
|
||||||
Application.getInstance().getMachineManager().activeStackChanged.connect(self._onStackChanged)
|
Application.getInstance().getMachineManager().activeStackChanged.connect(self._onStackChanged)
|
||||||
|
|
||||||
|
# Enable and disable extruder
|
||||||
|
Application.getInstance().getMachineManager().extruderChanged.connect(self.updateNodeBoundaryCheck)
|
||||||
|
|
||||||
# list of settings which were updated
|
# list of settings which were updated
|
||||||
self._changed_settings_since_last_rebuild = []
|
self._changed_settings_since_last_rebuild = []
|
||||||
|
|
||||||
|
@ -133,6 +136,7 @@ class BuildVolume(SceneNode):
|
||||||
if active_extruder_changed is not None:
|
if active_extruder_changed is not None:
|
||||||
node.callDecoration("getActiveExtruderChangedSignal").disconnect(self._updateDisallowedAreasAndRebuild)
|
node.callDecoration("getActiveExtruderChangedSignal").disconnect(self._updateDisallowedAreasAndRebuild)
|
||||||
node.decoratorsChanged.disconnect(self._updateNodeListeners)
|
node.decoratorsChanged.disconnect(self._updateNodeListeners)
|
||||||
|
self._updateDisallowedAreasAndRebuild() # make sure we didn't miss anything before we updated the node listeners
|
||||||
|
|
||||||
self._scene_objects = new_scene_objects
|
self._scene_objects = new_scene_objects
|
||||||
self._onSettingPropertyChanged("print_sequence", "value") # Create fake event, so right settings are triggered.
|
self._onSettingPropertyChanged("print_sequence", "value") # Create fake event, so right settings are triggered.
|
||||||
|
@ -147,7 +151,6 @@ class BuildVolume(SceneNode):
|
||||||
active_extruder_changed = node.callDecoration("getActiveExtruderChangedSignal")
|
active_extruder_changed = node.callDecoration("getActiveExtruderChangedSignal")
|
||||||
if active_extruder_changed is not None:
|
if active_extruder_changed is not None:
|
||||||
active_extruder_changed.connect(self._updateDisallowedAreasAndRebuild)
|
active_extruder_changed.connect(self._updateDisallowedAreasAndRebuild)
|
||||||
self._updateDisallowedAreasAndRebuild()
|
|
||||||
|
|
||||||
def setWidth(self, width):
|
def setWidth(self, width):
|
||||||
if width is not None:
|
if width is not None:
|
||||||
|
@ -217,30 +220,35 @@ class BuildVolume(SceneNode):
|
||||||
group_nodes.append(node) # Keep list of affected group_nodes
|
group_nodes.append(node) # Keep list of affected group_nodes
|
||||||
|
|
||||||
if node.callDecoration("isSliceable") or node.callDecoration("isGroup"):
|
if node.callDecoration("isSliceable") or node.callDecoration("isGroup"):
|
||||||
node._outside_buildarea = False
|
if node.collidesWithBbox(build_volume_bounding_box):
|
||||||
bbox = node.getBoundingBox()
|
node.setOutsideBuildArea(True)
|
||||||
|
|
||||||
# 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._outside_buildarea = True
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
convex_hull = node.callDecoration("getConvexHull")
|
if node.collidesWithArea(self.getDisallowedAreas()):
|
||||||
if convex_hull:
|
node.setOutsideBuildArea(True)
|
||||||
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
|
continue
|
||||||
node._outside_buildarea = True
|
|
||||||
|
# Mark the node as outside build volume if the set extruder is disabled
|
||||||
|
extruder_position = node.callDecoration("getActiveExtruderPosition")
|
||||||
|
if not self._global_container_stack.extruders[extruder_position].isEnabled:
|
||||||
|
node.setOutsideBuildArea(True)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
node.setOutsideBuildArea(False)
|
||||||
|
|
||||||
# Group nodes should override the _outside_buildarea property of their children.
|
# Group nodes should override the _outside_buildarea property of their children.
|
||||||
for group_node in group_nodes:
|
for group_node in group_nodes:
|
||||||
for child_node in group_node.getAllChildren():
|
children = group_node.getAllChildren()
|
||||||
child_node._outside_buildarea = group_node._outside_buildarea
|
|
||||||
|
# Check if one or more children are non-printable and if so, set the parent as non-printable:
|
||||||
|
for child_node in children:
|
||||||
|
if child_node.isOutsideBuildArea():
|
||||||
|
group_node.setOutsideBuildArea(True)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Apply results of the check to all children of the group:
|
||||||
|
for child_node in children:
|
||||||
|
child_node.setOutsideBuildArea(group_node.isOutsideBuildArea())
|
||||||
|
|
||||||
## Update the outsideBuildArea of a single node, given bounds or current build volume
|
## Update the outsideBuildArea of a single node, given bounds or current build volume
|
||||||
def checkBoundsAndUpdate(self, node: CuraSceneNode, bounds: Optional[AxisAlignedBox] = None):
|
def checkBoundsAndUpdate(self, node: CuraSceneNode, bounds: Optional[AxisAlignedBox] = None):
|
||||||
|
@ -260,24 +268,20 @@ class BuildVolume(SceneNode):
|
||||||
build_volume_bounding_box = bounds
|
build_volume_bounding_box = bounds
|
||||||
|
|
||||||
if node.callDecoration("isSliceable") or node.callDecoration("isGroup"):
|
if node.callDecoration("isSliceable") or node.callDecoration("isGroup"):
|
||||||
bbox = node.getBoundingBox()
|
if node.collidesWithBbox(build_volume_bounding_box):
|
||||||
|
|
||||||
# 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)
|
node.setOutsideBuildArea(True)
|
||||||
return
|
return
|
||||||
|
|
||||||
convex_hull = self.callDecoration("getConvexHull")
|
if node.collidesWithArea(self.getDisallowedAreas()):
|
||||||
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)
|
node.setOutsideBuildArea(True)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Mark the node as outside build volume if the set extruder is disabled
|
||||||
|
extruder_position = node.callDecoration("getActiveExtruderPosition")
|
||||||
|
if not self._global_container_stack.extruders[extruder_position].isEnabled:
|
||||||
|
node.setOutsideBuildArea(True)
|
||||||
|
return
|
||||||
|
|
||||||
node.setOutsideBuildArea(False)
|
node.setOutsideBuildArea(False)
|
||||||
|
|
||||||
## Recalculates the build volume & disallowed areas.
|
## Recalculates the build volume & disallowed areas.
|
||||||
|
|
|
@ -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.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import platform
|
import platform
|
||||||
|
@ -14,10 +14,11 @@ import urllib.request
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QUrl
|
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, Qt, QUrl
|
||||||
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton
|
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton
|
||||||
from PyQt5.QtGui import QDesktopServices
|
from PyQt5.QtGui import QDesktopServices
|
||||||
|
|
||||||
|
from UM.Resources import Resources
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.View.GL.OpenGL import OpenGL
|
from UM.View.GL.OpenGL import OpenGL
|
||||||
|
@ -84,14 +85,14 @@ class CrashHandler:
|
||||||
dialog = QDialog()
|
dialog = QDialog()
|
||||||
dialog.setMinimumWidth(500)
|
dialog.setMinimumWidth(500)
|
||||||
dialog.setMinimumHeight(170)
|
dialog.setMinimumHeight(170)
|
||||||
dialog.setWindowTitle(catalog.i18nc("@title:window", "Cura Crashed"))
|
dialog.setWindowTitle(catalog.i18nc("@title:window", "Cura can't startup"))
|
||||||
dialog.finished.connect(self._closeEarlyCrashDialog)
|
dialog.finished.connect(self._closeEarlyCrashDialog)
|
||||||
|
|
||||||
layout = QVBoxLayout(dialog)
|
layout = QVBoxLayout(dialog)
|
||||||
|
|
||||||
label = QLabel()
|
label = QLabel()
|
||||||
label.setText(catalog.i18nc("@label crash message", """<p><b>A fatal error has occurred.</p></b>
|
label.setText(catalog.i18nc("@label crash message", """<p><b>Oops, Ultimaker Cura has encountered something that doesn't seem right.</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>We 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>Backups can be found in the configuration folder.</p>
|
<p>Backups can be found in the configuration folder.</p>
|
||||||
<p>Please send us this Crash Report to fix the problem.</p>
|
<p>Please send us this Crash Report to fix the problem.</p>
|
||||||
"""))
|
"""))
|
||||||
|
@ -219,7 +220,7 @@ class CrashHandler:
|
||||||
|
|
||||||
def _messageWidget(self):
|
def _messageWidget(self):
|
||||||
label = QLabel()
|
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>
|
label.setText(catalog.i18nc("@label crash message", """<p><b>A fatal error has occurred in Cura. Please send us this Crash Report to fix the problem</p></b>
|
||||||
<p>Please use the "Send report" button to post a bug report automatically to our servers</p>
|
<p>Please use the "Send report" button to post a bug report automatically to our servers</p>
|
||||||
"""))
|
"""))
|
||||||
|
|
||||||
|
@ -258,7 +259,7 @@ class CrashHandler:
|
||||||
opengl_instance = OpenGL.getInstance()
|
opengl_instance = OpenGL.getInstance()
|
||||||
if not opengl_instance:
|
if not opengl_instance:
|
||||||
self.data["opengl"] = {"version": "n/a", "vendor": "n/a", "type": "n/a"}
|
self.data["opengl"] = {"version": "n/a", "vendor": "n/a", "type": "n/a"}
|
||||||
return catalog.i18nc("@label", "not yet initialised<br/>")
|
return catalog.i18nc("@label", "Not yet initialized<br/>")
|
||||||
|
|
||||||
info = "<ul>"
|
info = "<ul>"
|
||||||
info += catalog.i18nc("@label OpenGL version", "<li>OpenGL Version: {version}</li>").format(version = opengl_instance.getOpenGLVersion())
|
info += catalog.i18nc("@label OpenGL version", "<li>OpenGL Version: {version}</li>").format(version = opengl_instance.getOpenGLVersion())
|
||||||
|
|
|
@ -109,10 +109,6 @@ class CuraActions(QObject):
|
||||||
|
|
||||||
nodes_to_change = []
|
nodes_to_change = []
|
||||||
for node in Selection.getAllSelectedObjects():
|
for node in Selection.getAllSelectedObjects():
|
||||||
# Do not change any nodes that already have the right extruder set.
|
|
||||||
if node.callDecoration("getActiveExtruder") == extruder_id:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# If the node is a group, apply the active extruder to all children of the group.
|
# If the node is a group, apply the active extruder to all children of the group.
|
||||||
if node.callDecoration("isGroup"):
|
if node.callDecoration("isGroup"):
|
||||||
for grouped_node in BreadthFirstIterator(node):
|
for grouped_node in BreadthFirstIterator(node):
|
||||||
|
@ -125,6 +121,10 @@ class CuraActions(QObject):
|
||||||
nodes_to_change.append(grouped_node)
|
nodes_to_change.append(grouped_node)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Do not change any nodes that already have the right extruder set.
|
||||||
|
if node.callDecoration("getActiveExtruder") == extruder_id:
|
||||||
|
continue
|
||||||
|
|
||||||
nodes_to_change.append(node)
|
nodes_to_change.append(node)
|
||||||
|
|
||||||
if not nodes_to_change:
|
if not nodes_to_change:
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
#Type hinting.
|
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject
|
from PyQt5.QtCore import QObject, QTimer
|
||||||
from PyQt5.QtNetwork import QLocalServer
|
from PyQt5.QtNetwork import QLocalServer
|
||||||
from PyQt5.QtNetwork import QLocalSocket
|
from PyQt5.QtNetwork import QLocalSocket
|
||||||
|
|
||||||
|
@ -59,18 +57,22 @@ from cura.Machines.Models.BuildPlateModel import BuildPlateModel
|
||||||
from cura.Machines.Models.NozzleModel import NozzleModel
|
from cura.Machines.Models.NozzleModel import NozzleModel
|
||||||
from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel
|
from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel
|
||||||
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
|
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
|
||||||
|
|
||||||
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
|
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
|
||||||
|
|
||||||
from cura.Machines.Models.MaterialManagementModel import MaterialManagementModel
|
from cura.Machines.Models.MaterialManagementModel import MaterialManagementModel
|
||||||
from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
|
from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
|
||||||
from cura.Machines.Models.BrandMaterialsModel import BrandMaterialsModel
|
from cura.Machines.Models.BrandMaterialsModel import BrandMaterialsModel
|
||||||
|
from cura.Machines.Models.QualityManagementModel import QualityManagementModel
|
||||||
|
from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
|
||||||
|
from cura.Machines.Models.MachineManagementModel import MachineManagementModel
|
||||||
|
|
||||||
|
from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
|
||||||
|
|
||||||
|
from cura.Machines.MachineErrorChecker import MachineErrorChecker
|
||||||
|
|
||||||
from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
|
from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
|
||||||
from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
|
from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
|
||||||
|
|
||||||
from cura.Machines.VariantManager import VariantManager
|
from cura.Machines.VariantManager import VariantManager
|
||||||
from cura.Machines.Models.QualityManagementModel import QualityManagementModel
|
|
||||||
|
|
||||||
from . import PlatformPhysics
|
from . import PlatformPhysics
|
||||||
from . import BuildVolume
|
from . import BuildVolume
|
||||||
|
@ -87,7 +89,6 @@ from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
from cura.Settings.UserChangesModel import UserChangesModel
|
from cura.Settings.UserChangesModel import UserChangesModel
|
||||||
from cura.Settings.ExtrudersModel import ExtrudersModel
|
from cura.Settings.ExtrudersModel import ExtrudersModel
|
||||||
from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
|
from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
|
||||||
from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
|
|
||||||
from cura.Settings.ContainerManager import ContainerManager
|
from cura.Settings.ContainerManager import ContainerManager
|
||||||
|
|
||||||
from cura.ObjectsModel import ObjectsModel
|
from cura.ObjectsModel import ObjectsModel
|
||||||
|
@ -98,7 +99,6 @@ from PyQt5.QtGui import QColor, QIcon
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
|
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
|
||||||
|
|
||||||
from configparser import ConfigParser
|
|
||||||
import sys
|
import sys
|
||||||
import os.path
|
import os.path
|
||||||
import numpy
|
import numpy
|
||||||
|
@ -138,15 +138,10 @@ class CuraApplication(QtApplication):
|
||||||
MachineStack = Resources.UserType + 7
|
MachineStack = Resources.UserType + 7
|
||||||
ExtruderStack = Resources.UserType + 8
|
ExtruderStack = Resources.UserType + 8
|
||||||
DefinitionChangesContainer = Resources.UserType + 9
|
DefinitionChangesContainer = Resources.UserType + 9
|
||||||
|
SettingVisibilityPreset = Resources.UserType + 10
|
||||||
|
|
||||||
Q_ENUMS(ResourceTypes)
|
Q_ENUMS(ResourceTypes)
|
||||||
|
|
||||||
# FIXME: This signal belongs to the MachineManager, but the CuraEngineBackend plugin requires on it.
|
|
||||||
# Because plugins are initialized before the ContainerRegistry, putting this signal in MachineManager
|
|
||||||
# will make it initialized before ContainerRegistry does, and it won't find the active machine, thus
|
|
||||||
# Cura will always show the Add Machine Dialog upon start.
|
|
||||||
stacksValidationFinished = pyqtSignal() # Emitted whenever a validation is finished
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
# this list of dir names will be used by UM to detect an old cura directory
|
# 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"]:
|
for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "user", "variants"]:
|
||||||
|
@ -191,6 +186,7 @@ class CuraApplication(QtApplication):
|
||||||
Resources.addStorageType(self.ResourceTypes.ExtruderStack, "extruders")
|
Resources.addStorageType(self.ResourceTypes.ExtruderStack, "extruders")
|
||||||
Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances")
|
Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances")
|
||||||
Resources.addStorageType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
|
Resources.addStorageType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
|
||||||
|
Resources.addStorageType(self.ResourceTypes.SettingVisibilityPreset, "setting_visibility")
|
||||||
|
|
||||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer, "quality")
|
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer, "quality")
|
||||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer, "quality_changes")
|
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer, "quality_changes")
|
||||||
|
@ -223,12 +219,15 @@ class CuraApplication(QtApplication):
|
||||||
self._machine_manager = None # This is initialized on demand.
|
self._machine_manager = None # This is initialized on demand.
|
||||||
self._extruder_manager = None
|
self._extruder_manager = None
|
||||||
self._material_manager = None
|
self._material_manager = None
|
||||||
|
self._quality_manager = None
|
||||||
self._object_manager = None
|
self._object_manager = None
|
||||||
self._build_plate_model = None
|
self._build_plate_model = None
|
||||||
self._multi_build_plate_model = None
|
self._multi_build_plate_model = None
|
||||||
|
self._setting_visibility_presets_model = None
|
||||||
self._setting_inheritance_manager = None
|
self._setting_inheritance_manager = None
|
||||||
self._simple_mode_settings_manager = None
|
self._simple_mode_settings_manager = None
|
||||||
self._cura_scene_controller = None
|
self._cura_scene_controller = None
|
||||||
|
self._machine_error_checker = None
|
||||||
|
|
||||||
self._additional_components = {} # Components to add to certain areas in the interface
|
self._additional_components = {} # Components to add to certain areas in the interface
|
||||||
|
|
||||||
|
@ -285,10 +284,15 @@ class CuraApplication(QtApplication):
|
||||||
self._preferred_mimetype = ""
|
self._preferred_mimetype = ""
|
||||||
self._i18n_catalog = i18nCatalog("cura")
|
self._i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity)
|
self._update_platform_activity_timer = QTimer()
|
||||||
|
self._update_platform_activity_timer.setInterval(500)
|
||||||
|
self._update_platform_activity_timer.setSingleShot(True)
|
||||||
|
self._update_platform_activity_timer.timeout.connect(self.updatePlatformActivity)
|
||||||
|
|
||||||
|
self.getController().getScene().sceneChanged.connect(self.updatePlatformActivityDelayed)
|
||||||
self.getController().toolOperationStopped.connect(self._onToolOperationStopped)
|
self.getController().toolOperationStopped.connect(self._onToolOperationStopped)
|
||||||
self.getController().contextMenuRequested.connect(self._onContextMenuRequested)
|
self.getController().contextMenuRequested.connect(self._onContextMenuRequested)
|
||||||
self.getCuraSceneController().activeBuildPlateChanged.connect(self.updatePlatformActivity)
|
self.getCuraSceneController().activeBuildPlateChanged.connect(self.updatePlatformActivityDelayed)
|
||||||
|
|
||||||
Resources.addType(self.ResourceTypes.QmlFiles, "qml")
|
Resources.addType(self.ResourceTypes.QmlFiles, "qml")
|
||||||
Resources.addType(self.ResourceTypes.Firmware, "firmware")
|
Resources.addType(self.ResourceTypes.Firmware, "firmware")
|
||||||
|
@ -375,20 +379,6 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
preferences.setDefault("local_file/last_used_type", "text/x-gcode")
|
preferences.setDefault("local_file/last_used_type", "text/x-gcode")
|
||||||
|
|
||||||
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.applicationShuttingDown.connect(self.saveSettings)
|
||||||
self.engineCreatedSignal.connect(self._onEngineCreated)
|
self.engineCreatedSignal.connect(self._onEngineCreated)
|
||||||
|
|
||||||
|
@ -404,91 +394,6 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
CuraApplication.Created = True
|
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):
|
def _onEngineCreated(self):
|
||||||
self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
|
self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
|
||||||
|
@ -546,27 +451,18 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def discardOrKeepProfileChangesClosed(self, option):
|
def discardOrKeepProfileChangesClosed(self, option):
|
||||||
if option == "discard":
|
|
||||||
global_stack = self.getGlobalContainerStack()
|
global_stack = self.getGlobalContainerStack()
|
||||||
for extruder in self._extruder_manager.getMachineExtruders(global_stack.getId()):
|
if option == "discard":
|
||||||
extruder.getTop().clear()
|
for extruder in global_stack.extruders.values():
|
||||||
global_stack.getTop().clear()
|
extruder.userChanges.clear()
|
||||||
|
global_stack.userChanges.clear()
|
||||||
|
|
||||||
# if the user decided to keep settings then the user settings should be re-calculated and validated for errors
|
# if the user decided to keep settings then the user settings should be re-calculated and validated for errors
|
||||||
# before slicing. To ensure that slicer uses right settings values
|
# before slicing. To ensure that slicer uses right settings values
|
||||||
elif option == "keep":
|
elif option == "keep":
|
||||||
global_stack = self.getGlobalContainerStack()
|
for extruder in global_stack.extruders.values():
|
||||||
for extruder in self._extruder_manager.getMachineExtruders(global_stack.getId()):
|
extruder.userChanges.update()
|
||||||
user_extruder_container = extruder.getTop()
|
global_stack.userChanges.update()
|
||||||
if user_extruder_container:
|
|
||||||
user_extruder_container.update()
|
|
||||||
|
|
||||||
user_global_container = global_stack.getTop()
|
|
||||||
if user_global_container:
|
|
||||||
user_global_container.update()
|
|
||||||
|
|
||||||
# notify listeners that quality has changed (after user selected discard or keep)
|
|
||||||
self.getMachineManager().activeQualityChanged.emit()
|
|
||||||
|
|
||||||
@pyqtSlot(int)
|
@pyqtSlot(int)
|
||||||
def messageBoxClosed(self, button):
|
def messageBoxClosed(self, button):
|
||||||
|
@ -602,8 +498,13 @@ class CuraApplication(QtApplication):
|
||||||
def getStaticVersion(cls):
|
def getStaticVersion(cls):
|
||||||
return CuraVersion
|
return CuraVersion
|
||||||
|
|
||||||
|
## Handle removing the unneeded plugins
|
||||||
|
# \sa PluginRegistry
|
||||||
|
def _removePlugins(self):
|
||||||
|
self._plugin_registry.removePlugins()
|
||||||
|
|
||||||
## Handle loading of all plugin types (and the backend explicitly)
|
## Handle loading of all plugin types (and the backend explicitly)
|
||||||
# \sa PluginRegistery
|
# \sa PluginRegistry
|
||||||
def _loadPlugins(self):
|
def _loadPlugins(self):
|
||||||
self._plugin_registry.addType("profile_reader", self._addProfileReader)
|
self._plugin_registry.addType("profile_reader", self._addProfileReader)
|
||||||
self._plugin_registry.addType("profile_writer", self._addProfileWriter)
|
self._plugin_registry.addType("profile_writer", self._addProfileWriter)
|
||||||
|
@ -742,19 +643,28 @@ class CuraApplication(QtApplication):
|
||||||
self.preRun()
|
self.preRun()
|
||||||
|
|
||||||
container_registry = ContainerRegistry.getInstance()
|
container_registry = ContainerRegistry.getInstance()
|
||||||
|
|
||||||
|
Logger.log("i", "Initializing variant manager")
|
||||||
self._variant_manager = VariantManager(container_registry)
|
self._variant_manager = VariantManager(container_registry)
|
||||||
self._variant_manager.initialize()
|
self._variant_manager.initialize()
|
||||||
|
|
||||||
|
Logger.log("i", "Initializing material manager")
|
||||||
from cura.Machines.MaterialManager import MaterialManager
|
from cura.Machines.MaterialManager import MaterialManager
|
||||||
self._material_manager = MaterialManager(container_registry, parent = self)
|
self._material_manager = MaterialManager(container_registry, parent = self)
|
||||||
self._material_manager.initialize()
|
self._material_manager.initialize()
|
||||||
|
|
||||||
|
Logger.log("i", "Initializing quality manager")
|
||||||
from cura.Machines.QualityManager import QualityManager
|
from cura.Machines.QualityManager import QualityManager
|
||||||
self._quality_manager = QualityManager(container_registry, parent = self)
|
self._quality_manager = QualityManager(container_registry, parent = self)
|
||||||
self._quality_manager.initialize()
|
self._quality_manager.initialize()
|
||||||
|
|
||||||
|
Logger.log("i", "Initializing machine manager")
|
||||||
self._machine_manager = MachineManager(self)
|
self._machine_manager = MachineManager(self)
|
||||||
|
|
||||||
|
Logger.log("i", "Initializing machine error checker")
|
||||||
|
self._machine_error_checker = MachineErrorChecker(self)
|
||||||
|
self._machine_error_checker.initialize()
|
||||||
|
|
||||||
# Check if we should run as single instance or not
|
# Check if we should run as single instance or not
|
||||||
self._setUpSingleInstanceServer()
|
self._setUpSingleInstanceServer()
|
||||||
|
|
||||||
|
@ -767,6 +677,11 @@ class CuraApplication(QtApplication):
|
||||||
self._print_information = PrintInformation.PrintInformation()
|
self._print_information = PrintInformation.PrintInformation()
|
||||||
self._cura_actions = CuraActions.CuraActions(self)
|
self._cura_actions = CuraActions.CuraActions(self)
|
||||||
|
|
||||||
|
# Initialize setting visibility presets model
|
||||||
|
self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self)
|
||||||
|
default_visibility_profile = self._setting_visibility_presets_model.getItem(0)
|
||||||
|
Preferences.getInstance().setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"]))
|
||||||
|
|
||||||
# Detect in which mode to run and execute that mode
|
# Detect in which mode to run and execute that mode
|
||||||
if self.getCommandLineOption("headless", False):
|
if self.getCommandLineOption("headless", False):
|
||||||
self.runWithoutGUI()
|
self.runWithoutGUI()
|
||||||
|
@ -780,8 +695,11 @@ class CuraApplication(QtApplication):
|
||||||
self._openFile(file_name)
|
self._openFile(file_name)
|
||||||
|
|
||||||
self.started = True
|
self.started = True
|
||||||
|
self.initializationFinished.emit()
|
||||||
self.exec_()
|
self.exec_()
|
||||||
|
|
||||||
|
initializationFinished = pyqtSignal()
|
||||||
|
|
||||||
## Run Cura without GUI elements and interaction (server mode).
|
## Run Cura without GUI elements and interaction (server mode).
|
||||||
def runWithoutGUI(self):
|
def runWithoutGUI(self):
|
||||||
self._use_gui = False
|
self._use_gui = False
|
||||||
|
@ -846,6 +764,13 @@ class CuraApplication(QtApplication):
|
||||||
def hasGui(self):
|
def hasGui(self):
|
||||||
return self._use_gui
|
return self._use_gui
|
||||||
|
|
||||||
|
@pyqtSlot(result = QObject)
|
||||||
|
def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel:
|
||||||
|
return self._setting_visibility_presets_model
|
||||||
|
|
||||||
|
def getMachineErrorChecker(self, *args) -> MachineErrorChecker:
|
||||||
|
return self._machine_error_checker
|
||||||
|
|
||||||
def getMachineManager(self, *args) -> MachineManager:
|
def getMachineManager(self, *args) -> MachineManager:
|
||||||
if self._machine_manager is None:
|
if self._machine_manager is None:
|
||||||
self._machine_manager = MachineManager(self)
|
self._machine_manager = MachineManager(self)
|
||||||
|
@ -960,6 +885,7 @@ class CuraApplication(QtApplication):
|
||||||
qmlRegisterType(BrandMaterialsModel, "Cura", 1, 0, "BrandMaterialsModel")
|
qmlRegisterType(BrandMaterialsModel, "Cura", 1, 0, "BrandMaterialsModel")
|
||||||
qmlRegisterType(MaterialManagementModel, "Cura", 1, 0, "MaterialManagementModel")
|
qmlRegisterType(MaterialManagementModel, "Cura", 1, 0, "MaterialManagementModel")
|
||||||
qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel")
|
qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel")
|
||||||
|
qmlRegisterType(MachineManagementModel, "Cura", 1, 0, "MachineManagementModel")
|
||||||
|
|
||||||
qmlRegisterSingletonType(QualityProfilesDropDownMenuModel, "Cura", 1, 0,
|
qmlRegisterSingletonType(QualityProfilesDropDownMenuModel, "Cura", 1, 0,
|
||||||
"QualityProfilesDropDownMenuModel", self.getQualityProfilesDropDownMenuModel)
|
"QualityProfilesDropDownMenuModel", self.getQualityProfilesDropDownMenuModel)
|
||||||
|
@ -968,6 +894,7 @@ class CuraApplication(QtApplication):
|
||||||
qmlRegisterType(NozzleModel, "Cura", 1, 0, "NozzleModel")
|
qmlRegisterType(NozzleModel, "Cura", 1, 0, "NozzleModel")
|
||||||
|
|
||||||
qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
|
qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
|
||||||
|
qmlRegisterType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel")
|
||||||
qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
|
qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
|
||||||
qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
|
qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
|
||||||
qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel")
|
qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel")
|
||||||
|
@ -1047,6 +974,10 @@ class CuraApplication(QtApplication):
|
||||||
def getSceneBoundingBoxString(self):
|
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()}
|
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()}
|
||||||
|
|
||||||
|
def updatePlatformActivityDelayed(self, node = None):
|
||||||
|
if node is not None and node.getMeshData() is not None:
|
||||||
|
self._update_platform_activity_timer.start()
|
||||||
|
|
||||||
## Update scene bounding box for current build plate
|
## Update scene bounding box for current build plate
|
||||||
def updatePlatformActivity(self, node = None):
|
def updatePlatformActivity(self, node = None):
|
||||||
count = 0
|
count = 0
|
||||||
|
@ -1179,7 +1110,7 @@ class CuraApplication(QtApplication):
|
||||||
continue
|
continue
|
||||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||||
continue # Node that doesnt have a mesh and is not a group.
|
continue # Node that doesnt have a mesh and is not a group.
|
||||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
if node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent().callDecoration("isSliceable"):
|
||||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||||
if not node.isSelectable():
|
if not node.isSelectable():
|
||||||
continue # i.e. node with layer data
|
continue # i.e. node with layer data
|
||||||
|
@ -1354,8 +1285,11 @@ class CuraApplication(QtApplication):
|
||||||
def reloadAll(self):
|
def reloadAll(self):
|
||||||
Logger.log("i", "Reloading all loaded mesh data.")
|
Logger.log("i", "Reloading all loaded mesh data.")
|
||||||
nodes = []
|
nodes = []
|
||||||
|
has_merged_nodes = False
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||||
if not isinstance(node, CuraSceneNode) or not node.getMeshData() :
|
if not isinstance(node, CuraSceneNode) or not node.getMeshData() :
|
||||||
|
if node.getName() == 'MergedMesh':
|
||||||
|
has_merged_nodes = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
nodes.append(node)
|
nodes.append(node)
|
||||||
|
@ -1369,10 +1303,14 @@ class CuraApplication(QtApplication):
|
||||||
job = ReadMeshJob(file_name)
|
job = ReadMeshJob(file_name)
|
||||||
job._node = node
|
job._node = node
|
||||||
job.finished.connect(self._reloadMeshFinished)
|
job.finished.connect(self._reloadMeshFinished)
|
||||||
|
if has_merged_nodes:
|
||||||
|
job.finished.connect(self.updateOriginOfMergedMeshes)
|
||||||
|
|
||||||
job.start()
|
job.start()
|
||||||
else:
|
else:
|
||||||
Logger.log("w", "Unable to reload data because we don't have a filename.")
|
Logger.log("w", "Unable to reload data because we don't have a filename.")
|
||||||
|
|
||||||
|
|
||||||
## Get logging data of the backend engine
|
## Get logging data of the backend engine
|
||||||
# \returns \type{string} Logging data
|
# \returns \type{string} Logging data
|
||||||
@pyqtSlot(result = str)
|
@pyqtSlot(result = str)
|
||||||
|
@ -1442,6 +1380,58 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
# Use the previously found center of the group bounding box as the new location of the group
|
# Use the previously found center of the group bounding box as the new location of the group
|
||||||
group_node.setPosition(group_node.getBoundingBox().center)
|
group_node.setPosition(group_node.getBoundingBox().center)
|
||||||
|
group_node.setName("MergedMesh") # add a specific name to destinguis this node
|
||||||
|
|
||||||
|
|
||||||
|
## Updates origin position of all merged meshes
|
||||||
|
# \param jobNode \type{Job} empty object which passed which is required by JobQueue
|
||||||
|
def updateOriginOfMergedMeshes(self, jobNode):
|
||||||
|
group_nodes = []
|
||||||
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||||
|
if isinstance(node, CuraSceneNode) and node.getName() == "MergedMesh":
|
||||||
|
|
||||||
|
#checking by name might be not enough, the merged mesh should has "GroupDecorator" decorator
|
||||||
|
for decorator in node.getDecorators():
|
||||||
|
if isinstance(decorator, GroupDecorator):
|
||||||
|
group_nodes.append(node)
|
||||||
|
break
|
||||||
|
|
||||||
|
for group_node in group_nodes:
|
||||||
|
meshes = [node.getMeshData() for node in group_node.getAllChildren() if node.getMeshData()]
|
||||||
|
|
||||||
|
# Compute the center of the objects
|
||||||
|
object_centers = []
|
||||||
|
# Forget about the translation that the original objects have
|
||||||
|
zero_translation = Matrix(data=numpy.zeros(3))
|
||||||
|
for mesh, node in zip(meshes, group_node.getChildren()):
|
||||||
|
transformation = node.getLocalTransformation()
|
||||||
|
transformation.setTranslation(zero_translation)
|
||||||
|
transformed_mesh = mesh.getTransformed(transformation)
|
||||||
|
center = transformed_mesh.getCenterPosition()
|
||||||
|
if center is not None:
|
||||||
|
object_centers.append(center)
|
||||||
|
|
||||||
|
if object_centers and len(object_centers) > 0:
|
||||||
|
middle_x = sum([v.x for v in object_centers]) / len(object_centers)
|
||||||
|
middle_y = sum([v.y for v in object_centers]) / len(object_centers)
|
||||||
|
middle_z = sum([v.z for v in object_centers]) / len(object_centers)
|
||||||
|
offset = Vector(middle_x, middle_y, middle_z)
|
||||||
|
else:
|
||||||
|
offset = Vector(0, 0, 0)
|
||||||
|
|
||||||
|
# Move each node to the same position.
|
||||||
|
for mesh, node in zip(meshes, group_node.getChildren()):
|
||||||
|
transformation = node.getLocalTransformation()
|
||||||
|
transformation.setTranslation(zero_translation)
|
||||||
|
transformed_mesh = mesh.getTransformed(transformation)
|
||||||
|
|
||||||
|
# Align the object around its zero position
|
||||||
|
# and also apply the offset to center it inside the group.
|
||||||
|
node.setPosition(-transformed_mesh.getZeroPosition() - offset)
|
||||||
|
|
||||||
|
# Use the previously found center of the group bounding box as the new location of the group
|
||||||
|
group_node.setPosition(group_node.getBoundingBox().center)
|
||||||
|
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def groupSelected(self):
|
def groupSelected(self):
|
||||||
|
@ -1457,6 +1447,12 @@ class CuraApplication(QtApplication):
|
||||||
group_node.setPosition(center)
|
group_node.setPosition(center)
|
||||||
group_node.setCenterPosition(center)
|
group_node.setCenterPosition(center)
|
||||||
|
|
||||||
|
# Remove nodes that are directly parented to another selected node from the selection so they remain parented
|
||||||
|
selected_nodes = Selection.getAllSelectedObjects().copy()
|
||||||
|
for node in selected_nodes:
|
||||||
|
if node.getParent() in selected_nodes and not node.getParent().callDecoration("isGroup"):
|
||||||
|
Selection.remove(node)
|
||||||
|
|
||||||
# Move selected nodes into the group-node
|
# Move selected nodes into the group-node
|
||||||
Selection.applyOperation(SetParentOperation, group_node)
|
Selection.applyOperation(SetParentOperation, group_node)
|
||||||
|
|
||||||
|
@ -1475,6 +1471,10 @@ class CuraApplication(QtApplication):
|
||||||
group_parent = node.getParent()
|
group_parent = node.getParent()
|
||||||
children = node.getChildren().copy()
|
children = node.getChildren().copy()
|
||||||
for child in children:
|
for child in children:
|
||||||
|
# Ungroup only 1 level deep
|
||||||
|
if child.getParent() != node:
|
||||||
|
continue
|
||||||
|
|
||||||
# Set the parent of the children to the parent of the group-node
|
# Set the parent of the children to the parent of the group-node
|
||||||
op.addOperation(SetParentOperation(child, group_parent))
|
op.addOperation(SetParentOperation(child, group_parent))
|
||||||
|
|
||||||
|
@ -1604,6 +1604,8 @@ class CuraApplication(QtApplication):
|
||||||
fixed_nodes.append(node_)
|
fixed_nodes.append(node_)
|
||||||
arranger = Arrange.create(fixed_nodes = fixed_nodes)
|
arranger = Arrange.create(fixed_nodes = fixed_nodes)
|
||||||
min_offset = 8
|
min_offset = 8
|
||||||
|
default_extruder_position = self.getMachineManager().defaultExtruderPosition
|
||||||
|
default_extruder_id = self._global_container_stack.extruders[default_extruder_position].getId()
|
||||||
|
|
||||||
for original_node in nodes:
|
for original_node in nodes:
|
||||||
|
|
||||||
|
@ -1669,6 +1671,8 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
op = AddSceneNodeOperation(node, scene.getRoot())
|
op = AddSceneNodeOperation(node, scene.getRoot())
|
||||||
op.push()
|
op.push()
|
||||||
|
|
||||||
|
node.callDecoration("setActiveExtruder", default_extruder_id)
|
||||||
scene.sceneChanged.emit(node)
|
scene.sceneChanged.emit(node)
|
||||||
|
|
||||||
self.fileCompleted.emit(filename)
|
self.fileCompleted.emit(filename)
|
||||||
|
@ -1689,7 +1693,7 @@ class CuraApplication(QtApplication):
|
||||||
result = workspace_reader.preRead(file_path, show_dialog=False)
|
result = workspace_reader.preRead(file_path, show_dialog=False)
|
||||||
return result == WorkspaceReader.PreReadResult.accepted
|
return result == WorkspaceReader.PreReadResult.accepted
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
Logger.log("e", "Could not check file %s: %s", file_url, e)
|
Logger.logException("e", "Could not check file %s: %s", file_url)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _onContextMenuRequested(self, x: float, y: float) -> None:
|
def _onContextMenuRequested(self, x: float, y: float) -> None:
|
||||||
|
|
181
cura/Machines/MachineErrorChecker.py
Normal file
181
cura/Machines/MachineErrorChecker.py
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtProperty
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Settings.SettingDefinition import SettingDefinition
|
||||||
|
from UM.Settings.Validator import ValidatorState
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# This class performs setting error checks for the currently active machine.
|
||||||
|
#
|
||||||
|
# The whole error checking process is pretty heavy which can take ~0.5 secs, so it can cause GUI to lag.
|
||||||
|
# The idea here is to split the whole error check into small tasks, each of which only checks a single setting key
|
||||||
|
# in a stack. According to my profiling results, the maximal runtime for such a sub-task is <0.03 secs, which should
|
||||||
|
# be good enough. Moreover, if any changes happened to the machine, we can cancel the check in progress without wait
|
||||||
|
# for it to finish the complete work.
|
||||||
|
#
|
||||||
|
class MachineErrorChecker(QObject):
|
||||||
|
|
||||||
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self._global_stack = None
|
||||||
|
|
||||||
|
self._has_errors = True # Result of the error check, indicating whether there are errors in the stack
|
||||||
|
self._error_keys = set() # A set of settings keys that have errors
|
||||||
|
self._error_keys_in_progress = set() # The variable that stores the results of the currently in progress check
|
||||||
|
|
||||||
|
self._stacks_and_keys_to_check = None # a FIFO queue of tuples (stack, key) to check for errors
|
||||||
|
|
||||||
|
self._need_to_check = False # Whether we need to schedule a new check or not. This flag is set when a new
|
||||||
|
# error check needs to take place while there is already one running at the moment.
|
||||||
|
self._check_in_progress = False # Whether there is an error check running in progress at the moment.
|
||||||
|
|
||||||
|
self._application = Application.getInstance()
|
||||||
|
self._machine_manager = self._application.getMachineManager()
|
||||||
|
|
||||||
|
self._start_time = 0 # measure checking time
|
||||||
|
|
||||||
|
# This timer delays the starting of error check so we can react less frequently if the user is frequently
|
||||||
|
# changing settings.
|
||||||
|
self._error_check_timer = QTimer(self)
|
||||||
|
self._error_check_timer.setInterval(100)
|
||||||
|
self._error_check_timer.setSingleShot(True)
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
self._error_check_timer.timeout.connect(self._rescheduleCheck)
|
||||||
|
|
||||||
|
# Reconnect all signals when the active machine gets changed.
|
||||||
|
self._machine_manager.globalContainerChanged.connect(self._onMachineChanged)
|
||||||
|
|
||||||
|
# Whenever the machine settings get changed, we schedule an error check.
|
||||||
|
self._machine_manager.globalContainerChanged.connect(self.startErrorCheck)
|
||||||
|
self._machine_manager.globalValueChanged.connect(self.startErrorCheck)
|
||||||
|
|
||||||
|
self._onMachineChanged()
|
||||||
|
|
||||||
|
def _onMachineChanged(self):
|
||||||
|
if self._global_stack:
|
||||||
|
self._global_stack.propertyChanged.disconnect(self.startErrorCheck)
|
||||||
|
self._global_stack.containersChanged.disconnect(self.startErrorCheck)
|
||||||
|
|
||||||
|
for extruder in self._global_stack.extruders.values():
|
||||||
|
extruder.propertyChanged.disconnect(self.startErrorCheck)
|
||||||
|
extruder.containersChanged.disconnect(self.startErrorCheck)
|
||||||
|
|
||||||
|
self._global_stack = self._machine_manager.activeMachine
|
||||||
|
|
||||||
|
if self._global_stack:
|
||||||
|
self._global_stack.propertyChanged.connect(self.startErrorCheck)
|
||||||
|
self._global_stack.containersChanged.connect(self.startErrorCheck)
|
||||||
|
|
||||||
|
for extruder in self._global_stack.extruders.values():
|
||||||
|
extruder.propertyChanged.connect(self.startErrorCheck)
|
||||||
|
extruder.containersChanged.connect(self.startErrorCheck)
|
||||||
|
|
||||||
|
hasErrorUpdated = pyqtSignal()
|
||||||
|
needToWaitForResultChanged = pyqtSignal()
|
||||||
|
errorCheckFinished = pyqtSignal()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = hasErrorUpdated)
|
||||||
|
def hasError(self) -> bool:
|
||||||
|
return self._has_errors
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = needToWaitForResultChanged)
|
||||||
|
def needToWaitForResult(self) -> bool:
|
||||||
|
return self._need_to_check or self._check_in_progress
|
||||||
|
|
||||||
|
# Starts the error check timer to schedule a new error check.
|
||||||
|
def startErrorCheck(self, *args):
|
||||||
|
if not self._check_in_progress:
|
||||||
|
self._need_to_check = True
|
||||||
|
self.needToWaitForResultChanged.emit()
|
||||||
|
self._error_check_timer.start()
|
||||||
|
|
||||||
|
# This function is called by the timer to reschedule a new error check.
|
||||||
|
# If there is no check in progress, it will start a new one. If there is any, it sets the "_need_to_check" flag
|
||||||
|
# to notify the current check to stop and start a new one.
|
||||||
|
def _rescheduleCheck(self):
|
||||||
|
if self._check_in_progress and not self._need_to_check:
|
||||||
|
self._need_to_check = True
|
||||||
|
self.needToWaitForResultChanged.emit()
|
||||||
|
return
|
||||||
|
|
||||||
|
self._error_keys_in_progress = set()
|
||||||
|
self._need_to_check = False
|
||||||
|
self.needToWaitForResultChanged.emit()
|
||||||
|
|
||||||
|
global_stack = self._machine_manager.activeMachine
|
||||||
|
if global_stack is None:
|
||||||
|
Logger.log("i", "No active machine, nothing to check.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Populate the (stack, key) tuples to check
|
||||||
|
self._stacks_and_keys_to_check = deque()
|
||||||
|
for stack in [global_stack] + list(global_stack.extruders.values()):
|
||||||
|
for key in stack.getAllKeys():
|
||||||
|
self._stacks_and_keys_to_check.append((stack, key))
|
||||||
|
|
||||||
|
self._application.callLater(self._checkStack)
|
||||||
|
self._start_time = time.time()
|
||||||
|
Logger.log("d", "New error check scheduled.")
|
||||||
|
|
||||||
|
def _checkStack(self):
|
||||||
|
if self._need_to_check:
|
||||||
|
Logger.log("d", "Need to check for errors again. Discard the current progress and reschedule a check.")
|
||||||
|
self._check_in_progress = False
|
||||||
|
self._application.callLater(self.startErrorCheck)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._check_in_progress = True
|
||||||
|
|
||||||
|
# If there is nothing to check any more, it means there is no error.
|
||||||
|
if not self._stacks_and_keys_to_check:
|
||||||
|
# Finish
|
||||||
|
self._setResult(False)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the next stack and key to check
|
||||||
|
stack, key = self._stacks_and_keys_to_check.popleft()
|
||||||
|
|
||||||
|
enabled = stack.getProperty(key, "enabled")
|
||||||
|
if not enabled:
|
||||||
|
self._application.callLater(self._checkStack)
|
||||||
|
return
|
||||||
|
|
||||||
|
validation_state = stack.getProperty(key, "validationState")
|
||||||
|
if validation_state is None:
|
||||||
|
# Setting is not validated. This can happen if there is only a setting definition.
|
||||||
|
# We do need to validate it, because a setting definitions value can be set by a function, which could
|
||||||
|
# be an invalid setting.
|
||||||
|
definition = stack.getSettingDefinition(key)
|
||||||
|
validator_type = SettingDefinition.getValidatorForType(definition.type)
|
||||||
|
if validator_type:
|
||||||
|
validator = validator_type(key)
|
||||||
|
validation_state = validator(stack)
|
||||||
|
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
|
||||||
|
# Finish
|
||||||
|
self._setResult(True)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Schedule the check for the next key
|
||||||
|
self._application.callLater(self._checkStack)
|
||||||
|
|
||||||
|
def _setResult(self, result: bool):
|
||||||
|
if result != self._has_errors:
|
||||||
|
self._has_errors = result
|
||||||
|
self.hasErrorUpdated.emit()
|
||||||
|
self._machine_manager.stacksValidationChanged.emit()
|
||||||
|
self._need_to_check = False
|
||||||
|
self._check_in_progress = False
|
||||||
|
self.needToWaitForResultChanged.emit()
|
||||||
|
self.errorCheckFinished.emit()
|
||||||
|
Logger.log("i", "Error check finished, result = %s, time = %0.1fs", result, time.time() - self._start_time)
|
|
@ -1,9 +1,10 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
from cura.Machines.MaterialNode import MaterialNode #For type checking.
|
||||||
|
|
||||||
#
|
## A MaterialGroup represents a group of material InstanceContainers that are derived from a single material profile.
|
||||||
# A MaterialGroup represents a group of material InstanceContainers that are derived from a single material profile.
|
|
||||||
# The main InstanceContainer which has the ID of the material profile file name is called the "root_material". For
|
# The main InstanceContainer which has the ID of the material profile file name is called the "root_material". For
|
||||||
# example: "generic_abs" is the root material (ID) of "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4",
|
# example: "generic_abs" is the root material (ID) of "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4",
|
||||||
# and "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4" are derived materials of "generic_abs".
|
# and "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4" are derived materials of "generic_abs".
|
||||||
|
@ -15,12 +16,13 @@
|
||||||
# so "generic_abs_ultimaker3", "generic_abs_ultimaker3_AA_0.4", etc.
|
# so "generic_abs_ultimaker3", "generic_abs_ultimaker3_AA_0.4", etc.
|
||||||
#
|
#
|
||||||
class MaterialGroup:
|
class MaterialGroup:
|
||||||
__slots__ = ("name", "root_material_node", "derived_material_node_list")
|
__slots__ = ("name", "is_read_only", "root_material_node", "derived_material_node_list")
|
||||||
|
|
||||||
def __init__(self, name: str):
|
def __init__(self, name: str, root_material_node: MaterialNode):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.root_material_node = None
|
self.is_read_only = False
|
||||||
self.derived_material_node_list = []
|
self.root_material_node = root_material_node
|
||||||
|
self.derived_material_node_list = [] #type: List[MaterialNode]
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return "%s[%s]" % (self.__class__.__name__, self.name)
|
return "%s[%s]" % (self.__class__.__name__, self.name)
|
||||||
|
|
|
@ -72,29 +72,28 @@ class MaterialManager(QObject):
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
# Find all materials and put them in a matrix for quick search.
|
# Find all materials and put them in a matrix for quick search.
|
||||||
material_metadata_list = self._container_registry.findContainersMetadata(type = "material")
|
material_metadatas = {metadata["id"]: metadata for metadata in self._container_registry.findContainersMetadata(type = "material")}
|
||||||
|
|
||||||
self._material_group_map = dict()
|
self._material_group_map = dict()
|
||||||
|
|
||||||
# Map #1
|
# Map #1
|
||||||
# root_material_id -> MaterialGroup
|
# root_material_id -> MaterialGroup
|
||||||
for material_metadata in material_metadata_list:
|
for material_id, material_metadata in material_metadatas.items():
|
||||||
material_id = material_metadata["id"]
|
|
||||||
# We don't store empty material in the lookup tables
|
# We don't store empty material in the lookup tables
|
||||||
if material_id == "empty_material":
|
if material_id == "empty_material":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
root_material_id = material_metadata.get("base_file")
|
root_material_id = material_metadata.get("base_file")
|
||||||
if root_material_id not in self._material_group_map:
|
if root_material_id not in self._material_group_map:
|
||||||
self._material_group_map[root_material_id] = MaterialGroup(root_material_id)
|
self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id]))
|
||||||
|
self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id)
|
||||||
group = self._material_group_map[root_material_id]
|
group = self._material_group_map[root_material_id]
|
||||||
|
|
||||||
# We only add root materials here
|
#Store this material in the group of the appropriate root material.
|
||||||
if material_id == root_material_id:
|
if material_id != root_material_id:
|
||||||
group.root_material_node = MaterialNode(material_metadata)
|
|
||||||
else:
|
|
||||||
new_node = MaterialNode(material_metadata)
|
new_node = MaterialNode(material_metadata)
|
||||||
group.derived_material_node_list.append(new_node)
|
group.derived_material_node_list.append(new_node)
|
||||||
|
|
||||||
# Order this map alphabetically so it's easier to navigate in a debugger
|
# Order this map alphabetically so it's easier to navigate in a debugger
|
||||||
self._material_group_map = OrderedDict(sorted(self._material_group_map.items(), key = lambda x: x[0]))
|
self._material_group_map = OrderedDict(sorted(self._material_group_map.items(), key = lambda x: x[0]))
|
||||||
|
|
||||||
|
@ -108,6 +107,7 @@ class MaterialManager(QObject):
|
||||||
# Map #2
|
# Map #2
|
||||||
# Lookup table for material type -> fallback material metadata, only for read-only materials
|
# Lookup table for material type -> fallback material metadata, only for read-only materials
|
||||||
grouped_by_type_dict = dict()
|
grouped_by_type_dict = dict()
|
||||||
|
material_types_without_fallback = set()
|
||||||
for root_material_id, material_node in self._material_group_map.items():
|
for root_material_id, material_node in self._material_group_map.items():
|
||||||
if not self._container_registry.isReadOnly(root_material_id):
|
if not self._container_registry.isReadOnly(root_material_id):
|
||||||
continue
|
continue
|
||||||
|
@ -115,6 +115,7 @@ class MaterialManager(QObject):
|
||||||
if material_type not in grouped_by_type_dict:
|
if material_type not in grouped_by_type_dict:
|
||||||
grouped_by_type_dict[material_type] = {"generic": None,
|
grouped_by_type_dict[material_type] = {"generic": None,
|
||||||
"others": []}
|
"others": []}
|
||||||
|
material_types_without_fallback.add(material_type)
|
||||||
brand = material_node.root_material_node.metadata["brand"]
|
brand = material_node.root_material_node.metadata["brand"]
|
||||||
if brand.lower() == "generic":
|
if brand.lower() == "generic":
|
||||||
to_add = True
|
to_add = True
|
||||||
|
@ -124,6 +125,10 @@ class MaterialManager(QObject):
|
||||||
to_add = False # don't add if it's not the default diameter
|
to_add = False # don't add if it's not the default diameter
|
||||||
if to_add:
|
if to_add:
|
||||||
grouped_by_type_dict[material_type] = material_node.root_material_node.metadata
|
grouped_by_type_dict[material_type] = material_node.root_material_node.metadata
|
||||||
|
material_types_without_fallback.remove(material_type)
|
||||||
|
# Remove the materials that have no fallback materials
|
||||||
|
for material_type in material_types_without_fallback:
|
||||||
|
del grouped_by_type_dict[material_type]
|
||||||
self._fallback_materials_map = grouped_by_type_dict
|
self._fallback_materials_map = grouped_by_type_dict
|
||||||
|
|
||||||
# Map #3
|
# Map #3
|
||||||
|
@ -172,7 +177,7 @@ class MaterialManager(QObject):
|
||||||
# "machine" -> "variant_name" -> "root material ID" -> specific material InstanceContainer
|
# "machine" -> "variant_name" -> "root material ID" -> specific material InstanceContainer
|
||||||
# Construct the "machine" -> "variant" -> "root material ID" -> specific material InstanceContainer
|
# Construct the "machine" -> "variant" -> "root material ID" -> specific material InstanceContainer
|
||||||
self._diameter_machine_variant_material_map = dict()
|
self._diameter_machine_variant_material_map = dict()
|
||||||
for material_metadata in material_metadata_list:
|
for material_metadata in material_metadatas.values():
|
||||||
# We don't store empty material in the lookup tables
|
# We don't store empty material in the lookup tables
|
||||||
if material_metadata["id"] == "empty_material":
|
if material_metadata["id"] == "empty_material":
|
||||||
continue
|
continue
|
||||||
|
@ -210,6 +215,7 @@ class MaterialManager(QObject):
|
||||||
self.materialsUpdated.emit()
|
self.materialsUpdated.emit()
|
||||||
|
|
||||||
def _updateMaps(self):
|
def _updateMaps(self):
|
||||||
|
Logger.log("i", "Updating material lookup data ...")
|
||||||
self.initialize()
|
self.initialize()
|
||||||
|
|
||||||
def _onContainerMetadataChanged(self, container):
|
def _onContainerMetadataChanged(self, container):
|
||||||
|
@ -325,6 +331,35 @@ class MaterialManager(QObject):
|
||||||
|
|
||||||
return material_node
|
return material_node
|
||||||
|
|
||||||
|
#
|
||||||
|
# Gets MaterialNode for the given extruder and machine with the given material type.
|
||||||
|
# Returns None if:
|
||||||
|
# 1. the given machine doesn't have materials;
|
||||||
|
# 2. cannot find any material InstanceContainers with the given settings.
|
||||||
|
#
|
||||||
|
def getMaterialNodeByType(self, global_stack: "GlobalStack", extruder_variant_name: str, material_guid: str) -> Optional["MaterialNode"]:
|
||||||
|
node = None
|
||||||
|
machine_definition = global_stack.definition
|
||||||
|
if parseBool(machine_definition.getMetaDataEntry("has_materials", False)):
|
||||||
|
material_diameter = machine_definition.getProperty("material_diameter", "value")
|
||||||
|
if isinstance(material_diameter, SettingFunction):
|
||||||
|
material_diameter = material_diameter(global_stack)
|
||||||
|
|
||||||
|
# Look at the guid to material dictionary
|
||||||
|
root_material_id = None
|
||||||
|
for material_group in self._guid_material_groups_map[material_guid]:
|
||||||
|
if material_group.is_read_only:
|
||||||
|
root_material_id = material_group.root_material_node.metadata["id"]
|
||||||
|
break
|
||||||
|
|
||||||
|
if not root_material_id:
|
||||||
|
Logger.log("i", "Cannot find materials with guid [%s] ", material_guid)
|
||||||
|
return None
|
||||||
|
|
||||||
|
node = self.getMaterialNode(machine_definition.getId(), extruder_variant_name,
|
||||||
|
material_diameter, root_material_id)
|
||||||
|
return node
|
||||||
|
|
||||||
#
|
#
|
||||||
# Used by QualityManager. Built-in quality profiles may be based on generic material IDs such as "generic_pla".
|
# Used by QualityManager. Built-in quality profiles may be based on generic material IDs such as "generic_pla".
|
||||||
# For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use
|
# For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use
|
||||||
|
@ -349,10 +384,10 @@ class MaterialManager(QObject):
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def getDefaultMaterial(self, global_stack: "GlobalStack", extruder_variant_name: str) -> Optional["MaterialNode"]:
|
def getDefaultMaterial(self, global_stack: "GlobalStack", extruder_variant_name: Optional[str]) -> Optional["MaterialNode"]:
|
||||||
node = None
|
node = None
|
||||||
machine_definition = global_stack.definition
|
machine_definition = global_stack.definition
|
||||||
if parseBool(machine_definition.getMetaDataEntry("has_materials", False)):
|
if parseBool(global_stack.getMetaDataEntry("has_materials", False)):
|
||||||
material_diameter = machine_definition.getProperty("material_diameter", "value")
|
material_diameter = machine_definition.getProperty("material_diameter", "value")
|
||||||
if isinstance(material_diameter, SettingFunction):
|
if isinstance(material_diameter, SettingFunction):
|
||||||
material_diameter = material_diameter(global_stack)
|
material_diameter = material_diameter(global_stack)
|
||||||
|
@ -363,6 +398,16 @@ class MaterialManager(QObject):
|
||||||
material_diameter, root_material_id)
|
material_diameter, root_material_id)
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
def removeMaterialByRootId(self, root_material_id: str):
|
||||||
|
material_group = self.getMaterialGroup(root_material_id)
|
||||||
|
if not material_group:
|
||||||
|
Logger.log("i", "Unable to remove the material with id %s, because it doesn't exist.", root_material_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
|
||||||
|
for node in nodes_to_remove:
|
||||||
|
self._container_registry.removeContainer(node.metadata["id"])
|
||||||
|
|
||||||
#
|
#
|
||||||
# Methods for GUI
|
# Methods for GUI
|
||||||
#
|
#
|
||||||
|
@ -386,14 +431,7 @@ class MaterialManager(QObject):
|
||||||
@pyqtSlot("QVariant")
|
@pyqtSlot("QVariant")
|
||||||
def removeMaterial(self, material_node: "MaterialNode"):
|
def removeMaterial(self, material_node: "MaterialNode"):
|
||||||
root_material_id = material_node.metadata["base_file"]
|
root_material_id = material_node.metadata["base_file"]
|
||||||
material_group = self.getMaterialGroup(root_material_id)
|
self.removeMaterialByRootId(root_material_id)
|
||||||
if not material_group:
|
|
||||||
Logger.log("d", "Unable to remove the material with id %s, because it doesn't exist.", root_material_id)
|
|
||||||
return
|
|
||||||
|
|
||||||
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
|
|
||||||
for node in nodes_to_remove:
|
|
||||||
self._container_registry.removeContainer(node.metadata["id"])
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Creates a duplicate of a material, which has the same GUID and base_file metadata.
|
# Creates a duplicate of a material, which has the same GUID and base_file metadata.
|
||||||
|
@ -460,8 +498,10 @@ class MaterialManager(QObject):
|
||||||
# Ensure all settings are saved.
|
# Ensure all settings are saved.
|
||||||
self._application.saveSettings()
|
self._application.saveSettings()
|
||||||
|
|
||||||
global_stack = self._application.getGlobalContainerStack()
|
machine_manager = self._application.getMachineManager()
|
||||||
approximate_diameter = str(round(global_stack.getProperty("material_diameter", "value")))
|
extruder_stack = machine_manager.activeStack
|
||||||
|
|
||||||
|
approximate_diameter = str(extruder_stack.approximateMaterialDiameter)
|
||||||
root_material_id = "generic_pla"
|
root_material_id = "generic_pla"
|
||||||
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter)
|
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter)
|
||||||
material_group = self.getMaterialGroup(root_material_id)
|
material_group = self.getMaterialGroup(root_material_id)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
|
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,6 +26,8 @@ class BaseMaterialsModel(ListModel):
|
||||||
|
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self._application = Application.getInstance()
|
||||||
|
self._machine_manager = self._application.getMachineManager()
|
||||||
|
|
||||||
self.addRoleName(self.RootMaterialIdRole, "root_material_id")
|
self.addRoleName(self.RootMaterialIdRole, "root_material_id")
|
||||||
self.addRoleName(self.IdRole, "id")
|
self.addRoleName(self.IdRole, "id")
|
||||||
|
@ -35,12 +38,31 @@ class BaseMaterialsModel(ListModel):
|
||||||
self.addRoleName(self.ContainerNodeRole, "container_node")
|
self.addRoleName(self.ContainerNodeRole, "container_node")
|
||||||
|
|
||||||
self._extruder_position = 0
|
self._extruder_position = 0
|
||||||
|
self._extruder_stack = None
|
||||||
|
|
||||||
|
def _updateExtruderStack(self):
|
||||||
|
global_stack = self._machine_manager.activeMachine
|
||||||
|
if global_stack is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._extruder_stack is not None:
|
||||||
|
self._extruder_stack.pyqtContainersChanged.disconnect(self._update)
|
||||||
|
self._extruder_stack = global_stack.extruders.get(str(self._extruder_position))
|
||||||
|
if self._extruder_stack is not None:
|
||||||
|
self._extruder_stack.pyqtContainersChanged.connect(self._update)
|
||||||
|
|
||||||
def setExtruderPosition(self, position: int):
|
def setExtruderPosition(self, position: int):
|
||||||
if self._extruder_position != position:
|
if self._extruder_position != position:
|
||||||
self._extruder_position = position
|
self._extruder_position = position
|
||||||
|
self._updateExtruderStack()
|
||||||
self.extruderPositionChanged.emit()
|
self.extruderPositionChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(int, fset = setExtruderPosition, notify = extruderPositionChanged)
|
@pyqtProperty(int, fset = setExtruderPosition, notify = extruderPositionChanged)
|
||||||
def extruderPosition(self) -> int:
|
def extruderPosition(self) -> int:
|
||||||
return self._extruder_positoin
|
return self._extruder_position
|
||||||
|
|
||||||
|
#
|
||||||
|
# This is an abstract method that needs to be implemented by
|
||||||
|
#
|
||||||
|
def _update(self):
|
||||||
|
pass
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
|
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
|
||||||
|
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
|
from UM.Logger import Logger
|
||||||
from .BaseMaterialsModel import BaseMaterialsModel
|
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -53,10 +53,8 @@ class BrandMaterialsModel(ListModel):
|
||||||
self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
|
self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
|
||||||
self._material_manager = CuraApplication.getInstance().getMaterialManager()
|
self._material_manager = CuraApplication.getInstance().getMaterialManager()
|
||||||
|
|
||||||
self._machine_manager.globalContainerChanged.connect(self._update)
|
self._machine_manager.activeStackChanged.connect(self._update) #Update when switching machines.
|
||||||
self._extruder_manager.activeExtruderChanged.connect(self._update)
|
self._material_manager.materialsUpdated.connect(self._update) #Update when the list of materials changes.
|
||||||
self._material_manager.materialsUpdated.connect(self._update)
|
|
||||||
|
|
||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
def setExtruderPosition(self, position: int):
|
def setExtruderPosition(self, position: int):
|
||||||
|
@ -69,6 +67,7 @@ class BrandMaterialsModel(ListModel):
|
||||||
return self._extruder_position
|
return self._extruder_position
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
|
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||||
global_stack = self._machine_manager.activeMachine
|
global_stack = self._machine_manager.activeMachine
|
||||||
if global_stack is None:
|
if global_stack is None:
|
||||||
self.setItems([])
|
self.setItems([])
|
||||||
|
@ -120,12 +119,19 @@ class BrandMaterialsModel(ListModel):
|
||||||
material_type_item = {"name": material_type,
|
material_type_item = {"name": material_type,
|
||||||
"colors": BaseMaterialsModel(self)}
|
"colors": BaseMaterialsModel(self)}
|
||||||
material_type_item["colors"].clear()
|
material_type_item["colors"].clear()
|
||||||
|
|
||||||
|
# Sort materials by name
|
||||||
|
material_list = sorted(material_list, key = lambda x: x["name"].upper())
|
||||||
material_type_item["colors"].setItems(material_list)
|
material_type_item["colors"].setItems(material_list)
|
||||||
|
|
||||||
material_type_item_list.append(material_type_item)
|
material_type_item_list.append(material_type_item)
|
||||||
|
|
||||||
|
# Sort material type by name
|
||||||
|
material_type_item_list = sorted(material_type_item_list, key = lambda x: x["name"].upper())
|
||||||
brand_item["materials"].setItems(material_type_item_list)
|
brand_item["materials"].setItems(material_type_item_list)
|
||||||
|
|
||||||
brand_item_list.append(brand_item)
|
brand_item_list.append(brand_item)
|
||||||
|
|
||||||
|
# Sort brand by name
|
||||||
|
brand_item_list = sorted(brand_item_list, key = lambda x: x["name"].upper())
|
||||||
self.setItems(brand_item_list)
|
self.setItems(brand_item_list)
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
from UM.Logger import Logger
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
from UM.Util import parseBool
|
from UM.Util import parseBool
|
||||||
|
|
||||||
|
@ -29,6 +30,7 @@ class BuildPlateModel(ListModel):
|
||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
|
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||||
global_stack = self._machine_manager._global_container_stack
|
global_stack = self._machine_manager._global_container_stack
|
||||||
if not global_stack:
|
if not global_stack:
|
||||||
self.setItems([])
|
self.setItems([])
|
||||||
|
|
|
@ -12,7 +12,7 @@ from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfile
|
||||||
class CustomQualityProfilesDropDownMenuModel(QualityProfilesDropDownMenuModel):
|
class CustomQualityProfilesDropDownMenuModel(QualityProfilesDropDownMenuModel):
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
Logger.log("d", "Updating %s ...", self.__class__.__name__)
|
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||||
|
|
||||||
active_global_stack = self._machine_manager.activeMachine
|
active_global_stack = self._machine_manager.activeMachine
|
||||||
if active_global_stack is None:
|
if active_global_stack is None:
|
||||||
|
@ -23,7 +23,7 @@ class CustomQualityProfilesDropDownMenuModel(QualityProfilesDropDownMenuModel):
|
||||||
quality_changes_group_dict = self._quality_manager.getQualityChangesGroups(active_global_stack)
|
quality_changes_group_dict = self._quality_manager.getQualityChangesGroups(active_global_stack)
|
||||||
|
|
||||||
item_list = []
|
item_list = []
|
||||||
for key in sorted(quality_changes_group_dict):
|
for key in sorted(quality_changes_group_dict, key = lambda name: name.upper()):
|
||||||
quality_changes_group = quality_changes_group_dict[key]
|
quality_changes_group = quality_changes_group_dict[key]
|
||||||
|
|
||||||
item = {"name": quality_changes_group.name,
|
item = {"name": quality_changes_group.name,
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from .BaseMaterialsModel import BaseMaterialsModel
|
from UM.Logger import Logger
|
||||||
|
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
||||||
|
|
||||||
|
|
||||||
class GenericMaterialsModel(BaseMaterialsModel):
|
class GenericMaterialsModel(BaseMaterialsModel):
|
||||||
|
@ -14,13 +15,13 @@ class GenericMaterialsModel(BaseMaterialsModel):
|
||||||
self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
|
self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
|
||||||
self._material_manager = CuraApplication.getInstance().getMaterialManager()
|
self._material_manager = CuraApplication.getInstance().getMaterialManager()
|
||||||
|
|
||||||
self._machine_manager.globalContainerChanged.connect(self._update)
|
self._machine_manager.activeStackChanged.connect(self._update) #Update when switching machines.
|
||||||
self._extruder_manager.activeExtruderChanged.connect(self._update)
|
self._material_manager.materialsUpdated.connect(self._update) #Update when the list of materials changes.
|
||||||
self._material_manager.materialsUpdated.connect(self._update)
|
|
||||||
|
|
||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
|
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||||
|
|
||||||
global_stack = self._machine_manager.activeMachine
|
global_stack = self._machine_manager.activeMachine
|
||||||
if global_stack is None:
|
if global_stack is None:
|
||||||
self.setItems([])
|
self.setItems([])
|
||||||
|
@ -55,6 +56,6 @@ class GenericMaterialsModel(BaseMaterialsModel):
|
||||||
item_list.append(item)
|
item_list.append(item)
|
||||||
|
|
||||||
# Sort the item list by material name alphabetically
|
# Sort the item list by material name alphabetically
|
||||||
item_list = sorted(item_list, key = lambda d: d["name"])
|
item_list = sorted(item_list, key = lambda d: d["name"].upper())
|
||||||
|
|
||||||
self.setItems(item_list)
|
self.setItems(item_list)
|
||||||
|
|
82
cura/Machines/Models/MachineManagementModel.py
Normal file
82
cura/Machines/Models/MachineManagementModel.py
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from UM.Qt.ListModel import ListModel
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# This the QML model for the quality management page.
|
||||||
|
#
|
||||||
|
class MachineManagementModel(ListModel):
|
||||||
|
NameRole = Qt.UserRole + 1
|
||||||
|
IdRole = Qt.UserRole + 2
|
||||||
|
MetaDataRole = Qt.UserRole + 3
|
||||||
|
GroupRole = Qt.UserRole + 4
|
||||||
|
|
||||||
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.addRoleName(self.NameRole, "name")
|
||||||
|
self.addRoleName(self.IdRole, "id")
|
||||||
|
self.addRoleName(self.MetaDataRole, "metadata")
|
||||||
|
self.addRoleName(self.GroupRole, "group")
|
||||||
|
self._local_container_stacks = []
|
||||||
|
self._network_container_stacks = []
|
||||||
|
|
||||||
|
# Listen to changes
|
||||||
|
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged)
|
||||||
|
ContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerChanged)
|
||||||
|
ContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged)
|
||||||
|
self._filter_dict = {}
|
||||||
|
self._update()
|
||||||
|
|
||||||
|
## Handler for container added/removed events from registry
|
||||||
|
def _onContainerChanged(self, container):
|
||||||
|
# We only need to update when the added / removed container is a stack.
|
||||||
|
if isinstance(container, ContainerStack) and container.getMetaDataEntry("type") == "machine":
|
||||||
|
self._update()
|
||||||
|
|
||||||
|
## Private convenience function to reset & repopulate the model.
|
||||||
|
def _update(self):
|
||||||
|
items = []
|
||||||
|
|
||||||
|
# Get first the network enabled printers
|
||||||
|
network_filter_printers = {"type": "machine",
|
||||||
|
"um_network_key": "*",
|
||||||
|
"hidden": "False"}
|
||||||
|
self._network_container_stacks = ContainerRegistry.getInstance().findContainerStacks(**network_filter_printers)
|
||||||
|
self._network_container_stacks.sort(key = lambda i: i.getMetaDataEntry("connect_group_name"))
|
||||||
|
|
||||||
|
for container in self._network_container_stacks:
|
||||||
|
metadata = container.getMetaData().copy()
|
||||||
|
if container.getBottom():
|
||||||
|
metadata["definition_name"] = container.getBottom().getName()
|
||||||
|
|
||||||
|
items.append({"name": metadata["connect_group_name"],
|
||||||
|
"id": container.getId(),
|
||||||
|
"metadata": metadata,
|
||||||
|
"group": catalog.i18nc("@info:title", "Network enabled printers")})
|
||||||
|
|
||||||
|
# Get now the local printers
|
||||||
|
local_filter_printers = {"type": "machine", "um_network_key": None}
|
||||||
|
self._local_container_stacks = ContainerRegistry.getInstance().findContainerStacks(**local_filter_printers)
|
||||||
|
self._local_container_stacks.sort(key = lambda i: i.getName())
|
||||||
|
|
||||||
|
for container in self._local_container_stacks:
|
||||||
|
metadata = container.getMetaData().copy()
|
||||||
|
if container.getBottom():
|
||||||
|
metadata["definition_name"] = container.getBottom().getName()
|
||||||
|
|
||||||
|
items.append({"name": container.getName(),
|
||||||
|
"id": container.getId(),
|
||||||
|
"metadata": metadata,
|
||||||
|
"group": catalog.i18nc("@info:title", "Local printers")})
|
||||||
|
|
||||||
|
self.setItems(items)
|
|
@ -1,8 +1,9 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, pyqtProperty
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
from UM.Logger import Logger
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,6 +61,8 @@ class MaterialManagementModel(ListModel):
|
||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
|
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||||
|
|
||||||
global_stack = self._machine_manager.activeMachine
|
global_stack = self._machine_manager.activeMachine
|
||||||
if global_stack is None:
|
if global_stack is None:
|
||||||
self.setItems([])
|
self.setItems([])
|
||||||
|
@ -97,5 +100,5 @@ class MaterialManagementModel(ListModel):
|
||||||
|
|
||||||
material_list.append(item)
|
material_list.append(item)
|
||||||
|
|
||||||
material_list = sorted(material_list, key = lambda k: (k["brand"].lower(), k["name"]))
|
material_list = sorted(material_list, key = lambda k: (k["brand"].upper(), k["name"].upper()))
|
||||||
self.setItems(material_list)
|
self.setItems(material_list)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty
|
from PyQt5.QtCore import QTimer, pyqtSignal, pyqtProperty
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Scene.Selection import Selection
|
from UM.Scene.Selection import Selection
|
||||||
|
@ -21,8 +21,13 @@ class MultiBuildPlateModel(ListModel):
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self._update_timer = QTimer()
|
||||||
|
self._update_timer.setInterval(100)
|
||||||
|
self._update_timer.setSingleShot(True)
|
||||||
|
self._update_timer.timeout.connect(self._updateSelectedObjectBuildPlateNumbers)
|
||||||
|
|
||||||
self._application = Application.getInstance()
|
self._application = Application.getInstance()
|
||||||
self._application.getController().getScene().sceneChanged.connect(self._updateSelectedObjectBuildPlateNumbers)
|
self._application.getController().getScene().sceneChanged.connect(self._updateSelectedObjectBuildPlateNumbersDelayed)
|
||||||
Selection.selectionChanged.connect(self._updateSelectedObjectBuildPlateNumbers)
|
Selection.selectionChanged.connect(self._updateSelectedObjectBuildPlateNumbers)
|
||||||
|
|
||||||
self._max_build_plate = 1 # default
|
self._max_build_plate = 1 # default
|
||||||
|
@ -45,6 +50,9 @@ class MultiBuildPlateModel(ListModel):
|
||||||
def activeBuildPlate(self):
|
def activeBuildPlate(self):
|
||||||
return self._active_build_plate
|
return self._active_build_plate
|
||||||
|
|
||||||
|
def _updateSelectedObjectBuildPlateNumbersDelayed(self, *args):
|
||||||
|
self._update_timer.start()
|
||||||
|
|
||||||
def _updateSelectedObjectBuildPlateNumbers(self, *args):
|
def _updateSelectedObjectBuildPlateNumbers(self, *args):
|
||||||
result = set()
|
result = set()
|
||||||
for node in Selection.getAllSelectedObjects():
|
for node in Selection.getAllSelectedObjects():
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
from UM.Logger import Logger
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
from UM.Util import parseBool
|
from UM.Util import parseBool
|
||||||
|
|
||||||
|
@ -20,32 +21,36 @@ class NozzleModel(ListModel):
|
||||||
self.addRoleName(self.HotendNameRole, "hotend_name")
|
self.addRoleName(self.HotendNameRole, "hotend_name")
|
||||||
self.addRoleName(self.ContainerNodeRole, "container_node")
|
self.addRoleName(self.ContainerNodeRole, "container_node")
|
||||||
|
|
||||||
Application.getInstance().globalContainerStackChanged.connect(self._update)
|
self._application = Application.getInstance()
|
||||||
Application.getInstance().getMachineManager().activeVariantChanged.connect(self._update)
|
self._machine_manager = self._application.getMachineManager()
|
||||||
Application.getInstance().getMachineManager().activeStackChanged.connect(self._update)
|
self._variant_manager = self._application.getVariantManager()
|
||||||
Application.getInstance().getMachineManager().activeMaterialChanged.connect(self._update)
|
|
||||||
|
self._machine_manager.globalContainerChanged.connect(self._update)
|
||||||
|
self._update()
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
|
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||||
|
|
||||||
self.items.clear()
|
self.items.clear()
|
||||||
|
|
||||||
variant_manager = Application.getInstance()._variant_manager
|
global_stack = self._machine_manager.activeMachine
|
||||||
active_global_stack = Application.getInstance().getMachineManager()._global_container_stack
|
if global_stack is None:
|
||||||
if active_global_stack is None:
|
|
||||||
self.setItems([])
|
self.setItems([])
|
||||||
return
|
return
|
||||||
|
|
||||||
has_variants = parseBool(active_global_stack.getMetaDataEntry("has_variants", False))
|
has_variants = parseBool(global_stack.getMetaDataEntry("has_variants", False))
|
||||||
if not has_variants:
|
if not has_variants:
|
||||||
self.setItems([])
|
self.setItems([])
|
||||||
return
|
return
|
||||||
|
|
||||||
variant_node_dict = variant_manager.getVariantNodes(active_global_stack)
|
from cura.Machines.VariantManager import VariantType
|
||||||
|
variant_node_dict = self._variant_manager.getVariantNodes(global_stack, VariantType.NOZZLE)
|
||||||
if not variant_node_dict:
|
if not variant_node_dict:
|
||||||
self.setItems([])
|
self.setItems([])
|
||||||
return
|
return
|
||||||
|
|
||||||
item_list = []
|
item_list = []
|
||||||
for hotend_name, container_node in sorted(variant_node_dict.items(), key = lambda i: i[0]):
|
for hotend_name, container_node in sorted(variant_node_dict.items(), key = lambda i: i[0].upper()):
|
||||||
item = {"id": hotend_name,
|
item = {"id": hotend_name,
|
||||||
"hotend_name": hotend_name,
|
"hotend_name": hotend_name,
|
||||||
"container_node": container_node
|
"container_node": container_node
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
from PyQt5.QtCore import Qt, pyqtSlot
|
from PyQt5.QtCore import Qt, pyqtSlot
|
||||||
|
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
|
from UM.Logger import Logger
|
||||||
|
|
||||||
#
|
#
|
||||||
# This the QML model for the quality management page.
|
# This the QML model for the quality management page.
|
||||||
|
@ -35,6 +35,8 @@ class QualityManagementModel(ListModel):
|
||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
|
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||||
|
|
||||||
global_stack = self._machine_manager.activeMachine
|
global_stack = self._machine_manager.activeMachine
|
||||||
|
|
||||||
quality_group_dict = self._quality_manager.getQualityGroups(global_stack)
|
quality_group_dict = self._quality_manager.getQualityGroups(global_stack)
|
||||||
|
@ -59,7 +61,7 @@ class QualityManagementModel(ListModel):
|
||||||
"quality_changes_group": None}
|
"quality_changes_group": None}
|
||||||
item_list.append(item)
|
item_list.append(item)
|
||||||
# Sort by quality names
|
# Sort by quality names
|
||||||
item_list = sorted(item_list, key = lambda x: x["name"])
|
item_list = sorted(item_list, key = lambda x: x["name"].upper())
|
||||||
|
|
||||||
# Create quality_changes group items
|
# Create quality_changes group items
|
||||||
quality_changes_item_list = []
|
quality_changes_item_list = []
|
||||||
|
@ -74,7 +76,7 @@ class QualityManagementModel(ListModel):
|
||||||
quality_changes_item_list.append(item)
|
quality_changes_item_list.append(item)
|
||||||
|
|
||||||
# Sort quality_changes items by names and append to the item list
|
# Sort quality_changes items by names and append to the item list
|
||||||
quality_changes_item_list = sorted(quality_changes_item_list, key = lambda x: x["name"])
|
quality_changes_item_list = sorted(quality_changes_item_list, key = lambda x: x["name"].upper())
|
||||||
item_list += quality_changes_item_list
|
item_list += quality_changes_item_list
|
||||||
|
|
||||||
self.setItems(item_list)
|
self.setItems(item_list)
|
||||||
|
|
|
@ -29,7 +29,7 @@ class QualityProfilesDropDownMenuModel(ListModel):
|
||||||
self.addRoleName(self.QualityTypeRole, "quality_type")
|
self.addRoleName(self.QualityTypeRole, "quality_type")
|
||||||
self.addRoleName(self.LayerHeightRole, "layer_height")
|
self.addRoleName(self.LayerHeightRole, "layer_height")
|
||||||
self.addRoleName(self.LayerHeightUnitRole, "layer_height_unit")
|
self.addRoleName(self.LayerHeightUnitRole, "layer_height_unit")
|
||||||
self.addRoleName(self.AvailableRole, "available")
|
self.addRoleName(self.AvailableRole, "available") #Whether the quality profile is available in our current nozzle + material.
|
||||||
self.addRoleName(self.QualityGroupRole, "quality_group")
|
self.addRoleName(self.QualityGroupRole, "quality_group")
|
||||||
self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
|
self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
|
||||||
|
|
||||||
|
@ -39,6 +39,7 @@ class QualityProfilesDropDownMenuModel(ListModel):
|
||||||
|
|
||||||
self._application.globalContainerStackChanged.connect(self._update)
|
self._application.globalContainerStackChanged.connect(self._update)
|
||||||
self._machine_manager.activeQualityGroupChanged.connect(self._update)
|
self._machine_manager.activeQualityGroupChanged.connect(self._update)
|
||||||
|
self._machine_manager.extruderChanged.connect(self._update)
|
||||||
self._quality_manager.qualitiesUpdated.connect(self._update)
|
self._quality_manager.qualitiesUpdated.connect(self._update)
|
||||||
|
|
||||||
self._layer_height_unit = "" # This is cached
|
self._layer_height_unit = "" # This is cached
|
||||||
|
@ -46,7 +47,7 @@ class QualityProfilesDropDownMenuModel(ListModel):
|
||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
Logger.log("d", "Updating quality profile model ...")
|
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||||
|
|
||||||
global_stack = self._machine_manager.activeMachine
|
global_stack = self._machine_manager.activeMachine
|
||||||
if global_stack is None:
|
if global_stack is None:
|
||||||
|
|
|
@ -21,6 +21,8 @@ class QualitySettingsModel(ListModel):
|
||||||
UserValueRole = Qt.UserRole + 6
|
UserValueRole = Qt.UserRole + 6
|
||||||
CategoryRole = Qt.UserRole + 7
|
CategoryRole = Qt.UserRole + 7
|
||||||
|
|
||||||
|
GLOBAL_STACK_POSITION = -1
|
||||||
|
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
super().__init__(parent = parent)
|
super().__init__(parent = parent)
|
||||||
|
|
||||||
|
@ -36,8 +38,7 @@ class QualitySettingsModel(ListModel):
|
||||||
self._application = Application.getInstance()
|
self._application = Application.getInstance()
|
||||||
self._quality_manager = self._application.getQualityManager()
|
self._quality_manager = self._application.getQualityManager()
|
||||||
|
|
||||||
self._selected_position = "" # empty string means GlobalStack
|
self._selected_position = self.GLOBAL_STACK_POSITION #Must be either GLOBAL_STACK_POSITION or an extruder position (0, 1, etc.)
|
||||||
# strings such as "0", "1", etc. mean extruder positions
|
|
||||||
self._selected_quality_item = None # The selected quality in the quality management page
|
self._selected_quality_item = None # The selected quality in the quality management page
|
||||||
self._i18n_catalog = None
|
self._i18n_catalog = None
|
||||||
|
|
||||||
|
@ -54,7 +55,7 @@ class QualitySettingsModel(ListModel):
|
||||||
self.selectedPositionChanged.emit()
|
self.selectedPositionChanged.emit()
|
||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
@pyqtProperty(str, fset = setSelectedPosition, notify = selectedPositionChanged)
|
@pyqtProperty(int, fset = setSelectedPosition, notify = selectedPositionChanged)
|
||||||
def selectedPosition(self):
|
def selectedPosition(self):
|
||||||
return self._selected_position
|
return self._selected_position
|
||||||
|
|
||||||
|
@ -69,7 +70,9 @@ class QualitySettingsModel(ListModel):
|
||||||
return self._selected_quality_item
|
return self._selected_quality_item
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
if self._selected_quality_item is None:
|
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||||
|
|
||||||
|
if not self._selected_quality_item:
|
||||||
self.setItems([])
|
self.setItems([])
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -81,24 +84,26 @@ class QualitySettingsModel(ListModel):
|
||||||
quality_group = self._selected_quality_item["quality_group"]
|
quality_group = self._selected_quality_item["quality_group"]
|
||||||
quality_changes_group = self._selected_quality_item["quality_changes_group"]
|
quality_changes_group = self._selected_quality_item["quality_changes_group"]
|
||||||
|
|
||||||
if self._selected_position == "":
|
if self._selected_position == self.GLOBAL_STACK_POSITION:
|
||||||
quality_node = quality_group.node_for_global
|
quality_node = quality_group.node_for_global
|
||||||
else:
|
else:
|
||||||
quality_node = quality_group.nodes_for_extruders.get(self._selected_position)
|
quality_node = quality_group.nodes_for_extruders.get(str(self._selected_position))
|
||||||
settings_keys = quality_group.getAllKeys()
|
settings_keys = quality_group.getAllKeys()
|
||||||
quality_containers = [quality_node.getContainer()]
|
quality_containers = []
|
||||||
|
if quality_node is not None:
|
||||||
|
quality_containers.append(quality_node.getContainer())
|
||||||
|
|
||||||
# Here, if the user has selected a quality changes, then "quality_changes_group" will not be None, and we fetch
|
# Here, if the user has selected a quality changes, then "quality_changes_group" will not be None, and we fetch
|
||||||
# the settings in that quality_changes_group.
|
# the settings in that quality_changes_group.
|
||||||
if quality_changes_group is not None:
|
if quality_changes_group is not None:
|
||||||
if self._selected_position == "":
|
if self._selected_position == self.GLOBAL_STACK_POSITION:
|
||||||
quality_changes_node = quality_changes_group.node_for_global
|
quality_changes_node = quality_changes_group.node_for_global
|
||||||
else:
|
else:
|
||||||
quality_changes_node = quality_changes_group.nodes_for_extruders.get(self._selected_position)
|
quality_changes_node = quality_changes_group.nodes_for_extruders.get(str(self._selected_position))
|
||||||
if quality_changes_node is not None: # it can be None if number of extruders are changed during runtime
|
if quality_changes_node is not None: # it can be None if number of extruders are changed during runtime
|
||||||
try:
|
try:
|
||||||
quality_containers.insert(0, quality_changes_node.getContainer())
|
quality_containers.insert(0, quality_changes_node.getContainer())
|
||||||
except:
|
except RuntimeError:
|
||||||
# FIXME: This is to prevent incomplete update of QualityManager
|
# FIXME: This is to prevent incomplete update of QualityManager
|
||||||
Logger.logException("d", "Failed to get container for quality changes node %s", quality_changes_node)
|
Logger.logException("d", "Failed to get container for quality changes node %s", quality_changes_node)
|
||||||
return
|
return
|
||||||
|
@ -125,7 +130,7 @@ class QualitySettingsModel(ListModel):
|
||||||
profile_value = new_value
|
profile_value = new_value
|
||||||
|
|
||||||
# Global tab should use resolve (if there is one)
|
# Global tab should use resolve (if there is one)
|
||||||
if self._selected_position == "":
|
if self._selected_position == self.GLOBAL_STACK_POSITION:
|
||||||
resolve_value = global_container_stack.getProperty(definition.key, "resolve")
|
resolve_value = global_container_stack.getProperty(definition.key, "resolve")
|
||||||
if resolve_value is not None and definition.key in settings_keys:
|
if resolve_value is not None and definition.key in settings_keys:
|
||||||
profile_value = resolve_value
|
profile_value = resolve_value
|
||||||
|
@ -133,10 +138,10 @@ class QualitySettingsModel(ListModel):
|
||||||
if profile_value is not None:
|
if profile_value is not None:
|
||||||
break
|
break
|
||||||
|
|
||||||
if not self._selected_position:
|
if self._selected_position == self.GLOBAL_STACK_POSITION:
|
||||||
user_value = global_container_stack.userChanges.getProperty(definition.key, "value")
|
user_value = global_container_stack.userChanges.getProperty(definition.key, "value")
|
||||||
else:
|
else:
|
||||||
extruder_stack = global_container_stack.extruders[self._selected_position]
|
extruder_stack = global_container_stack.extruders[str(self._selected_position)]
|
||||||
user_value = extruder_stack.userChanges.getProperty(definition.key, "value")
|
user_value = extruder_stack.userChanges.getProperty(definition.key, "value")
|
||||||
|
|
||||||
if profile_value is None and user_value is None:
|
if profile_value is None and user_value is None:
|
||||||
|
|
182
cura/Machines/Models/SettingVisibilityPresetsModel.py
Normal file
182
cura/Machines/Models/SettingVisibilityPresetsModel.py
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
import os
|
||||||
|
import urllib.parse
|
||||||
|
from configparser import ConfigParser
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtProperty, Qt, pyqtSignal, pyqtSlot
|
||||||
|
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Qt.ListModel import ListModel
|
||||||
|
from UM.Preferences import Preferences
|
||||||
|
from UM.Resources import Resources
|
||||||
|
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
class SettingVisibilityPresetsModel(ListModel):
|
||||||
|
IdRole = Qt.UserRole + 1
|
||||||
|
NameRole = Qt.UserRole + 2
|
||||||
|
SettingsRole = Qt.UserRole + 3
|
||||||
|
|
||||||
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.addRoleName(self.IdRole, "id")
|
||||||
|
self.addRoleName(self.NameRole, "name")
|
||||||
|
self.addRoleName(self.SettingsRole, "settings")
|
||||||
|
|
||||||
|
self._populate()
|
||||||
|
basic_item = self.items[1]
|
||||||
|
basic_visibile_settings = ";".join(basic_item["settings"])
|
||||||
|
|
||||||
|
self._preferences = Preferences.getInstance()
|
||||||
|
# Preference to store which preset is currently selected
|
||||||
|
self._preferences.addPreference("cura/active_setting_visibility_preset", "basic")
|
||||||
|
# Preference that stores the "custom" set so it can always be restored (even after a restart)
|
||||||
|
self._preferences.addPreference("cura/custom_visible_settings", basic_visibile_settings)
|
||||||
|
self._preferences.preferenceChanged.connect(self._onPreferencesChanged)
|
||||||
|
|
||||||
|
self._active_preset_item = self._getItem(self._preferences.getValue("cura/active_setting_visibility_preset"))
|
||||||
|
# Initialize visible settings if it is not done yet
|
||||||
|
visible_settings = self._preferences.getValue("general/visible_settings")
|
||||||
|
if not visible_settings:
|
||||||
|
self._preferences.setValue("general/visible_settings", ";".join(self._active_preset_item["settings"]))
|
||||||
|
else:
|
||||||
|
self._onPreferencesChanged("general/visible_settings")
|
||||||
|
|
||||||
|
self.activePresetChanged.emit()
|
||||||
|
|
||||||
|
def _getItem(self, item_id: str) -> Optional[dict]:
|
||||||
|
result = None
|
||||||
|
for item in self.items:
|
||||||
|
if item["id"] == item_id:
|
||||||
|
result = item
|
||||||
|
break
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _populate(self):
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
items = []
|
||||||
|
for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
|
||||||
|
try:
|
||||||
|
mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path)
|
||||||
|
except MimeTypeNotFoundError:
|
||||||
|
Logger.log("e", "Could not determine mime type of file %s", file_path)
|
||||||
|
continue
|
||||||
|
|
||||||
|
item_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_path)))
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
Logger.log("e", "[%s] is not a file", 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") or not parser.has_option("general", "weight"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
settings = []
|
||||||
|
for section in parser.sections():
|
||||||
|
if section == 'general':
|
||||||
|
continue
|
||||||
|
|
||||||
|
settings.append(section)
|
||||||
|
for option in parser[section].keys():
|
||||||
|
settings.append(option)
|
||||||
|
|
||||||
|
items.append({
|
||||||
|
"id": item_id,
|
||||||
|
"name": catalog.i18nc("@action:inmenu", parser["general"]["name"]),
|
||||||
|
"weight": parser["general"]["weight"],
|
||||||
|
"settings": settings,
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
Logger.logException("e", "Failed to load setting preset %s", file_path)
|
||||||
|
|
||||||
|
items.sort(key = lambda k: (int(k["weight"]), k["id"]))
|
||||||
|
# Put "custom" at the top
|
||||||
|
items.insert(0, {"id": "custom",
|
||||||
|
"name": "Custom selection",
|
||||||
|
"weight": -100,
|
||||||
|
"settings": []})
|
||||||
|
|
||||||
|
self.setItems(items)
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def setActivePreset(self, preset_id: str):
|
||||||
|
if preset_id == self._active_preset_item["id"]:
|
||||||
|
Logger.log("d", "Same setting visibility preset [%s] selected, do nothing.", preset_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
preset_item = None
|
||||||
|
for item in self.items:
|
||||||
|
if item["id"] == preset_id:
|
||||||
|
preset_item = item
|
||||||
|
break
|
||||||
|
if preset_item is None:
|
||||||
|
Logger.log("w", "Tried to set active preset to unknown id [%s]", preset_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
need_to_save_to_custom = self._active_preset_item["id"] == "custom" and preset_id != "custom"
|
||||||
|
if need_to_save_to_custom:
|
||||||
|
# Save the current visibility settings to custom
|
||||||
|
current_visibility_string = self._preferences.getValue("general/visible_settings")
|
||||||
|
if current_visibility_string:
|
||||||
|
self._preferences.setValue("cura/custom_visible_settings", current_visibility_string)
|
||||||
|
|
||||||
|
new_visibility_string = ";".join(preset_item["settings"])
|
||||||
|
if preset_id == "custom":
|
||||||
|
# Get settings from the stored custom data
|
||||||
|
new_visibility_string = self._preferences.getValue("cura/custom_visible_settings")
|
||||||
|
if new_visibility_string is None:
|
||||||
|
new_visibility_string = self._preferences.getValue("general/visible_settings")
|
||||||
|
self._preferences.setValue("general/visible_settings", new_visibility_string)
|
||||||
|
|
||||||
|
self._preferences.setValue("cura/active_setting_visibility_preset", preset_id)
|
||||||
|
self._active_preset_item = preset_item
|
||||||
|
self.activePresetChanged.emit()
|
||||||
|
|
||||||
|
activePresetChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = activePresetChanged)
|
||||||
|
def activePreset(self) -> str:
|
||||||
|
return self._active_preset_item["id"]
|
||||||
|
|
||||||
|
def _onPreferencesChanged(self, name: str):
|
||||||
|
if name != "general/visible_settings":
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find the preset that matches with the current visible settings setup
|
||||||
|
visibility_string = self._preferences.getValue("general/visible_settings")
|
||||||
|
if not visibility_string:
|
||||||
|
return
|
||||||
|
|
||||||
|
visibility_set = set(visibility_string.split(";"))
|
||||||
|
matching_preset_item = None
|
||||||
|
for item in self.items:
|
||||||
|
if item["id"] == "custom":
|
||||||
|
continue
|
||||||
|
if set(item["settings"]) == visibility_set:
|
||||||
|
matching_preset_item = item
|
||||||
|
break
|
||||||
|
|
||||||
|
item_to_set = self._active_preset_item
|
||||||
|
if matching_preset_item is None:
|
||||||
|
# The new visibility setup is "custom" should be custom
|
||||||
|
if self._active_preset_item["id"] == "custom":
|
||||||
|
# We are already in custom, just save the settings
|
||||||
|
self._preferences.setValue("cura/custom_visible_settings", visibility_string)
|
||||||
|
else:
|
||||||
|
item_to_set = self.items[0] # 0 is custom
|
||||||
|
else:
|
||||||
|
item_to_set = matching_preset_item
|
||||||
|
|
||||||
|
if self._active_preset_item is None or self._active_preset_item["id"] != item_to_set["id"]:
|
||||||
|
self._active_preset_item = item_to_set
|
||||||
|
self._preferences.setValue("cura/active_setting_visibility_preset", self._active_preset_item["id"])
|
||||||
|
self.activePresetChanged.emit()
|
|
@ -7,47 +7,21 @@ from .QualityGroup import QualityGroup
|
||||||
|
|
||||||
|
|
||||||
class QualityChangesGroup(QualityGroup):
|
class QualityChangesGroup(QualityGroup):
|
||||||
|
|
||||||
def __init__(self, name: str, quality_type: str, parent = None):
|
def __init__(self, name: str, quality_type: str, parent = None):
|
||||||
super().__init__(name, quality_type, parent)
|
super().__init__(name, quality_type, parent)
|
||||||
self._container_registry = Application.getInstance().getContainerRegistry()
|
self._container_registry = Application.getInstance().getContainerRegistry()
|
||||||
|
|
||||||
def addNode(self, node: "QualityNode"):
|
def addNode(self, node: "QualityNode"):
|
||||||
# TODO: in 3.2 and earlier, a quality_changes container may have a field called "extruder" which contains the
|
extruder_position = node.metadata.get("position")
|
||||||
# extruder definition ID it belongs to. But, in fact, we only need to know the following things:
|
if extruder_position is None: #Then we're a global quality changes profile.
|
||||||
# 1. which machine a custom profile is suitable for,
|
if self.node_for_global is not None:
|
||||||
# 2. if this profile is for the GlobalStack,
|
raise RuntimeError("{group} tries to overwrite the existing node_for_global {original_global} with {new_global}".format(group = self, original_global = self.node_for_global, new_global = node))
|
||||||
# 3. if this profile is for an ExtruderStack and which one (the position).
|
self.node_for_global = node
|
||||||
#
|
else: #This is an extruder's quality changes profile.
|
||||||
# So, it is preferred to have a field like this:
|
|
||||||
# extruder_position = 1
|
|
||||||
# instead of this:
|
|
||||||
# extruder = custom_extruder_1
|
|
||||||
#
|
|
||||||
# An upgrade needs to be done if we want to do it this way. Before that, we use the extruder's definition
|
|
||||||
# to figure out its position.
|
|
||||||
#
|
|
||||||
extruder_definition_id = node.metadata.get("extruder")
|
|
||||||
if extruder_definition_id:
|
|
||||||
metadata_list = self._container_registry.findDefinitionContainersMetadata(id = extruder_definition_id)
|
|
||||||
if not metadata_list:
|
|
||||||
raise RuntimeError("%s cannot get metadata for extruder definition [%s]" %
|
|
||||||
(self, extruder_definition_id))
|
|
||||||
extruder_definition_metadata = metadata_list[0]
|
|
||||||
extruder_position = str(extruder_definition_metadata["position"])
|
|
||||||
|
|
||||||
if extruder_position in self.nodes_for_extruders:
|
if extruder_position in self.nodes_for_extruders:
|
||||||
raise RuntimeError("%s tries to overwrite the existing nodes_for_extruders position [%s] %s with %s" %
|
raise RuntimeError("%s tries to overwrite the existing nodes_for_extruders position [%s] %s with %s" %
|
||||||
(self, extruder_position, self.node_for_global, node))
|
(self, extruder_position, self.node_for_global, node))
|
||||||
|
|
||||||
self.nodes_for_extruders[extruder_position] = node
|
self.nodes_for_extruders[extruder_position] = node
|
||||||
|
|
||||||
else:
|
|
||||||
# This is a quality_changes for the GlobalStack
|
|
||||||
if self.node_for_global is not None:
|
|
||||||
raise RuntimeError("%s tries to overwrite the existing node_for_global %s with %s" %
|
|
||||||
(self, self.node_for_global, node))
|
|
||||||
self.node_for_global = node
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return "%s[<%s>, available = %s]" % (self.__class__.__name__, self.name, self.is_available)
|
return "%s[<%s>, available = %s]" % (self.__class__.__name__, self.name, self.is_available)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Optional, List
|
from typing import Dict, Optional, List
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtSlot
|
from PyQt5.QtCore import QObject, pyqtSlot
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ class QualityGroup(QObject):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.name = name
|
self.name = name
|
||||||
self.node_for_global = None # type: Optional["QualityGroup"]
|
self.node_for_global = None # type: Optional["QualityGroup"]
|
||||||
self.nodes_for_extruders = dict() # position str -> QualityGroup
|
self.nodes_for_extruders = {} # type: Dict[int, "QualityGroup"]
|
||||||
self.quality_type = quality_type
|
self.quality_type = quality_type
|
||||||
self.is_available = False
|
self.is_available = False
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ from .QualityGroup import QualityGroup
|
||||||
from .QualityNode import QualityNode
|
from .QualityNode import QualityNode
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
from .QualityChangesGroup import QualityChangesGroup
|
from .QualityChangesGroup import QualityChangesGroup
|
||||||
|
|
||||||
|
@ -159,8 +160,8 @@ class QualityManager(QObject):
|
||||||
# Updates the given quality groups' availabilities according to which extruders are being used/ enabled.
|
# Updates the given quality groups' availabilities according to which extruders are being used/ enabled.
|
||||||
def _updateQualityGroupsAvailability(self, machine: "GlobalStack", quality_group_list):
|
def _updateQualityGroupsAvailability(self, machine: "GlobalStack", quality_group_list):
|
||||||
used_extruders = set()
|
used_extruders = set()
|
||||||
# TODO: This will change after the Machine refactoring
|
|
||||||
for i in range(machine.getProperty("machine_extruder_count", "value")):
|
for i in range(machine.getProperty("machine_extruder_count", "value")):
|
||||||
|
if machine.extruders[str(i)].isEnabled:
|
||||||
used_extruders.add(str(i))
|
used_extruders.add(str(i))
|
||||||
|
|
||||||
# Update the "is_available" flag for each quality group.
|
# Update the "is_available" flag for each quality group.
|
||||||
|
@ -178,7 +179,7 @@ class QualityManager(QObject):
|
||||||
|
|
||||||
# Returns a dict of "custom profile name" -> QualityChangesGroup
|
# Returns a dict of "custom profile name" -> QualityChangesGroup
|
||||||
def getQualityChangesGroups(self, machine: "GlobalStack") -> dict:
|
def getQualityChangesGroups(self, machine: "GlobalStack") -> dict:
|
||||||
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine)
|
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
|
||||||
|
|
||||||
machine_node = self._machine_quality_type_to_quality_changes_dict.get(machine_definition_id)
|
machine_node = self._machine_quality_type_to_quality_changes_dict.get(machine_definition_id)
|
||||||
if not machine_node:
|
if not machine_node:
|
||||||
|
@ -206,7 +207,7 @@ class QualityManager(QObject):
|
||||||
# For more details, see QualityGroup.
|
# For more details, see QualityGroup.
|
||||||
#
|
#
|
||||||
def getQualityGroups(self, machine: "GlobalStack") -> dict:
|
def getQualityGroups(self, machine: "GlobalStack") -> dict:
|
||||||
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine)
|
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
|
||||||
|
|
||||||
# This determines if we should only get the global qualities for the global stack and skip the global qualities for the extruder stacks
|
# This determines if we should only get the global qualities for the global stack and skip the global qualities for the extruder stacks
|
||||||
has_variant_materials = parseBool(machine.getMetaDataEntry("has_variant_materials", False))
|
has_variant_materials = parseBool(machine.getMetaDataEntry("has_variant_materials", False))
|
||||||
|
@ -315,7 +316,7 @@ class QualityManager(QObject):
|
||||||
return quality_group_dict
|
return quality_group_dict
|
||||||
|
|
||||||
def getQualityGroupsForMachineDefinition(self, machine: "GlobalStack") -> dict:
|
def getQualityGroupsForMachineDefinition(self, machine: "GlobalStack") -> dict:
|
||||||
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine)
|
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
|
||||||
|
|
||||||
# To find the quality container for the GlobalStack, check in the following fall-back manner:
|
# To find the quality container for the GlobalStack, check in the following fall-back manner:
|
||||||
# (1) the machine-specific node
|
# (1) the machine-specific node
|
||||||
|
@ -386,7 +387,7 @@ class QualityManager(QObject):
|
||||||
if quality_changes_group is None:
|
if quality_changes_group is None:
|
||||||
# create global quality changes only
|
# create global quality changes only
|
||||||
new_quality_changes = self._createQualityChanges(quality_group.quality_type, quality_changes_name,
|
new_quality_changes = self._createQualityChanges(quality_group.quality_type, quality_changes_name,
|
||||||
global_stack, extruder_id = None)
|
global_stack, None)
|
||||||
self._container_registry.addContainer(new_quality_changes)
|
self._container_registry.addContainer(new_quality_changes)
|
||||||
else:
|
else:
|
||||||
new_name = self._container_registry.uniqueName(quality_changes_name)
|
new_name = self._container_registry.uniqueName(quality_changes_name)
|
||||||
|
@ -428,11 +429,11 @@ class QualityManager(QObject):
|
||||||
Logger.log("w", "No quality or quality changes container found in stack %s, ignoring it", stack.getId())
|
Logger.log("w", "No quality or quality changes container found in stack %s, ignoring it", stack.getId())
|
||||||
continue
|
continue
|
||||||
|
|
||||||
extruder_definition_id = None
|
|
||||||
if isinstance(stack, ExtruderStack):
|
|
||||||
extruder_definition_id = stack.definition.getId()
|
|
||||||
quality_type = quality_container.getMetaDataEntry("quality_type")
|
quality_type = quality_container.getMetaDataEntry("quality_type")
|
||||||
new_changes = self._createQualityChanges(quality_type, unique_name, global_stack, extruder_definition_id)
|
extruder_stack = None
|
||||||
|
if isinstance(stack, ExtruderStack):
|
||||||
|
extruder_stack = stack
|
||||||
|
new_changes = self._createQualityChanges(quality_type, unique_name, global_stack, extruder_stack)
|
||||||
from cura.Settings.ContainerManager import ContainerManager
|
from cura.Settings.ContainerManager import ContainerManager
|
||||||
ContainerManager.getInstance()._performMerge(new_changes, quality_changes_container, clear_settings = False)
|
ContainerManager.getInstance()._performMerge(new_changes, quality_changes_container, clear_settings = False)
|
||||||
ContainerManager.getInstance()._performMerge(new_changes, user_container)
|
ContainerManager.getInstance()._performMerge(new_changes, user_container)
|
||||||
|
@ -443,8 +444,8 @@ class QualityManager(QObject):
|
||||||
# Create a quality changes container with the given setup.
|
# Create a quality changes container with the given setup.
|
||||||
#
|
#
|
||||||
def _createQualityChanges(self, quality_type: str, new_name: str, machine: "GlobalStack",
|
def _createQualityChanges(self, quality_type: str, new_name: str, machine: "GlobalStack",
|
||||||
extruder_id: Optional[str]) -> "InstanceContainer":
|
extruder_stack: Optional["ExtruderStack"]) -> "InstanceContainer":
|
||||||
base_id = machine.definition.getId() if extruder_id is None else extruder_id
|
base_id = machine.definition.getId() if extruder_stack is None else extruder_stack.getId()
|
||||||
new_id = base_id + "_" + new_name
|
new_id = base_id + "_" + new_name
|
||||||
new_id = new_id.lower().replace(" ", "_")
|
new_id = new_id.lower().replace(" ", "_")
|
||||||
new_id = self._container_registry.uniqueName(new_id)
|
new_id = self._container_registry.uniqueName(new_id)
|
||||||
|
@ -456,11 +457,11 @@ class QualityManager(QObject):
|
||||||
quality_changes.addMetaDataEntry("quality_type", quality_type)
|
quality_changes.addMetaDataEntry("quality_type", quality_type)
|
||||||
|
|
||||||
# If we are creating a container for an extruder, ensure we add that to the container
|
# If we are creating a container for an extruder, ensure we add that to the container
|
||||||
if extruder_id is not None:
|
if extruder_stack is not None:
|
||||||
quality_changes.addMetaDataEntry("extruder", extruder_id)
|
quality_changes.addMetaDataEntry("position", extruder_stack.getMetaDataEntry("position"))
|
||||||
|
|
||||||
# If the machine specifies qualities should be filtered, ensure we match the current criteria.
|
# If the machine specifies qualities should be filtered, ensure we match the current criteria.
|
||||||
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine)
|
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
|
||||||
quality_changes.setDefinition(machine_definition_id)
|
quality_changes.setDefinition(machine_definition_id)
|
||||||
|
|
||||||
quality_changes.addMetaDataEntry("setting_version", self._application.SettingVersion)
|
quality_changes.addMetaDataEntry("setting_version", self._application.SettingVersion)
|
||||||
|
@ -480,12 +481,13 @@ class QualityManager(QObject):
|
||||||
# Example: for an Ultimaker 3 Extended, it has "quality_definition = ultimaker3". This means Ultimaker 3 Extended
|
# Example: for an Ultimaker 3 Extended, it has "quality_definition = ultimaker3". This means Ultimaker 3 Extended
|
||||||
# shares the same set of qualities profiles as Ultimaker 3.
|
# shares the same set of qualities profiles as Ultimaker 3.
|
||||||
#
|
#
|
||||||
def getMachineDefinitionIDForQualitySearch(machine: "GlobalStack", default_definition_id: str = "fdmprinter") -> str:
|
def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainer",
|
||||||
|
default_definition_id: str = "fdmprinter") -> str:
|
||||||
machine_definition_id = default_definition_id
|
machine_definition_id = default_definition_id
|
||||||
if parseBool(machine.getMetaDataEntry("has_machine_quality", False)):
|
if parseBool(machine_definition.getMetaDataEntry("has_machine_quality", False)):
|
||||||
# Only use the machine's own quality definition ID if this machine has machine quality.
|
# Only use the machine's own quality definition ID if this machine has machine quality.
|
||||||
machine_definition_id = machine.getMetaDataEntry("quality_definition")
|
machine_definition_id = machine_definition.getMetaDataEntry("quality_definition")
|
||||||
if machine_definition_id is None:
|
if machine_definition_id is None:
|
||||||
machine_definition_id = machine.definition.getId()
|
machine_definition_id = machine_definition.getId()
|
||||||
|
|
||||||
return machine_definition_id
|
return machine_definition_id
|
||||||
|
|
|
@ -25,7 +25,7 @@ ALL_VARIANT_TYPES = (VariantType.BUILD_PLATE, VariantType.NOZZLE)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VariantManager is THE place to look for a specific variant. It maintains a variant lookup table with the following
|
# VariantManager is THE place to look for a specific variant. It maintains two variant lookup tables with the following
|
||||||
# structure:
|
# structure:
|
||||||
#
|
#
|
||||||
# [machine_definition_id] -> [variant_type] -> [variant_name] -> ContainerNode(metadata / container)
|
# [machine_definition_id] -> [variant_type] -> [variant_name] -> ContainerNode(metadata / container)
|
||||||
|
@ -35,6 +35,9 @@ ALL_VARIANT_TYPES = (VariantType.BUILD_PLATE, VariantType.NOZZLE)
|
||||||
# -> "BB 0.8"
|
# -> "BB 0.8"
|
||||||
# -> ...
|
# -> ...
|
||||||
#
|
#
|
||||||
|
# [machine_definition_id] -> [machine_buildplate_type] -> ContainerNode(metadata / container)
|
||||||
|
# Example: "ultimaker3" -> "glass" (this is different from the variant name) -> ContainerNode
|
||||||
|
#
|
||||||
# Note that the "container" field is not loaded in the beginning because it would defeat the purpose of lazy-loading.
|
# Note that the "container" field is not loaded in the beginning because it would defeat the purpose of lazy-loading.
|
||||||
# A container is loaded when getVariant() is called to load a variant InstanceContainer.
|
# A container is loaded when getVariant() is called to load a variant InstanceContainer.
|
||||||
#
|
#
|
||||||
|
@ -44,6 +47,7 @@ class VariantManager:
|
||||||
self._container_registry = container_registry # type: ContainerRegistry
|
self._container_registry = container_registry # type: ContainerRegistry
|
||||||
|
|
||||||
self._machine_to_variant_dict_map = dict() # <machine_type> -> <variant_dict>
|
self._machine_to_variant_dict_map = dict() # <machine_type> -> <variant_dict>
|
||||||
|
self._machine_to_buildplate_dict_map = dict()
|
||||||
|
|
||||||
self._exclude_variant_id_list = ["empty_variant"]
|
self._exclude_variant_id_list = ["empty_variant"]
|
||||||
|
|
||||||
|
@ -53,6 +57,7 @@ class VariantManager:
|
||||||
#
|
#
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
self._machine_to_variant_dict_map = OrderedDict()
|
self._machine_to_variant_dict_map = OrderedDict()
|
||||||
|
self._machine_to_buildplate_dict_map = OrderedDict()
|
||||||
|
|
||||||
# Cache all variants from the container registry to a variant map for better searching and organization.
|
# Cache all variants from the container registry to a variant map for better searching and organization.
|
||||||
variant_metadata_list = self._container_registry.findContainersMetadata(type = "variant")
|
variant_metadata_list = self._container_registry.findContainersMetadata(type = "variant")
|
||||||
|
@ -78,16 +83,40 @@ class VariantManager:
|
||||||
|
|
||||||
variant_dict[variant_name] = ContainerNode(metadata = variant_metadata)
|
variant_dict[variant_name] = ContainerNode(metadata = variant_metadata)
|
||||||
|
|
||||||
|
# If the variant is a buildplate then fill also the buildplate map
|
||||||
|
if variant_type == VariantType.BUILD_PLATE:
|
||||||
|
if variant_definition not in self._machine_to_buildplate_dict_map:
|
||||||
|
self._machine_to_buildplate_dict_map[variant_definition] = OrderedDict()
|
||||||
|
|
||||||
|
variant_container = self._container_registry.findContainers(type = "variant", id = variant_metadata["id"])
|
||||||
|
if not variant_container:
|
||||||
|
# ERROR: not variant container. This should never happen
|
||||||
|
raise RuntimeError("Not variant found [%s], type [%s] for machine [%s]" %
|
||||||
|
(variant_name, variant_type, variant_definition))
|
||||||
|
buildplate_type = variant_container[0].getProperty("machine_buildplate_type", "value")
|
||||||
|
if buildplate_type not in self._machine_to_buildplate_dict_map[variant_definition]:
|
||||||
|
self._machine_to_variant_dict_map[variant_definition][buildplate_type] = dict()
|
||||||
|
|
||||||
|
self._machine_to_buildplate_dict_map[variant_definition][buildplate_type] = variant_dict[variant_name]
|
||||||
|
|
||||||
#
|
#
|
||||||
# Gets the variant InstanceContainer with the given information.
|
# Gets the variant InstanceContainer with the given information.
|
||||||
# Almost the same as getVariantMetadata() except that this returns an InstanceContainer if present.
|
# Almost the same as getVariantMetadata() except that this returns an InstanceContainer if present.
|
||||||
#
|
#
|
||||||
def getVariantNode(self, machine_definition_id: str, variant_name: str,
|
def getVariantNode(self, machine_definition_id: str, variant_name: str,
|
||||||
variant_type: Optional["VariantType"] = VariantType.NOZZLE) -> Optional["ContainerNode"]:
|
variant_type: Optional["VariantType"] = None) -> Optional["ContainerNode"]:
|
||||||
|
if variant_type is None:
|
||||||
|
variant_node = None
|
||||||
|
variant_type_dict = self._machine_to_variant_dict_map[machine_definition_id]
|
||||||
|
for variant_dict in variant_type_dict.values():
|
||||||
|
if variant_name in variant_dict:
|
||||||
|
variant_node = variant_dict[variant_name]
|
||||||
|
break
|
||||||
|
return variant_node
|
||||||
return self._machine_to_variant_dict_map[machine_definition_id].get(variant_type, {}).get(variant_name)
|
return self._machine_to_variant_dict_map[machine_definition_id].get(variant_type, {}).get(variant_name)
|
||||||
|
|
||||||
def getVariantNodes(self, machine: "GlobalStack",
|
def getVariantNodes(self, machine: "GlobalStack",
|
||||||
variant_type: Optional["VariantType"] = VariantType.NOZZLE) -> dict:
|
variant_type: Optional["VariantType"] = None) -> dict:
|
||||||
machine_definition_id = machine.definition.getId()
|
machine_definition_id = machine.definition.getId()
|
||||||
return self._machine_to_variant_dict_map.get(machine_definition_id, {}).get(variant_type, {})
|
return self._machine_to_variant_dict_map.get(machine_definition_id, {}).get(variant_type, {})
|
||||||
|
|
||||||
|
@ -109,3 +138,8 @@ class VariantManager:
|
||||||
if preferred_variant_name:
|
if preferred_variant_name:
|
||||||
node = self.getVariantNode(machine_definition_id, preferred_variant_name, variant_type)
|
node = self.getVariantNode(machine_definition_id, preferred_variant_name, variant_type)
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
def getBuildplateVariantNode(self, machine_definition_id: str, buildplate_type: str) -> Optional["ContainerNode"]:
|
||||||
|
if machine_definition_id in self._machine_to_buildplate_dict_map:
|
||||||
|
return self._machine_to_buildplate_dict_map[machine_definition_id].get(buildplate_type)
|
||||||
|
return None
|
||||||
|
|
|
@ -33,6 +33,7 @@ class MultiplyObjectsJob(Job):
|
||||||
root = scene.getRoot()
|
root = scene.getRoot()
|
||||||
arranger = Arrange.create(scene_root=root)
|
arranger = Arrange.create(scene_root=root)
|
||||||
nodes = []
|
nodes = []
|
||||||
|
|
||||||
for node in self._objects:
|
for node in self._objects:
|
||||||
# If object is part of a group, multiply group
|
# If object is part of a group, multiply group
|
||||||
current_node = node
|
current_node = node
|
||||||
|
@ -49,18 +50,20 @@ class MultiplyObjectsJob(Job):
|
||||||
for i in range(self._count):
|
for i in range(self._count):
|
||||||
# We do place the nodes one by one, as we want to yield in between.
|
# We do place the nodes one by one, as we want to yield in between.
|
||||||
if not node_too_big:
|
if not node_too_big:
|
||||||
node, solution_found = arranger.findNodePlacement(current_node, offset_shape_arr, hull_shape_arr)
|
new_node, solution_found = arranger.findNodePlacement(current_node, offset_shape_arr, hull_shape_arr)
|
||||||
if node_too_big or not solution_found:
|
if node_too_big or not solution_found:
|
||||||
found_solution_for_all = False
|
found_solution_for_all = False
|
||||||
new_location = node.getPosition()
|
new_location = new_node.getPosition()
|
||||||
new_location = new_location.set(z = 100 - i * 20)
|
new_location = new_location.set(z = 100 - i * 20)
|
||||||
node.setPosition(new_location)
|
new_node.setPosition(new_location)
|
||||||
|
|
||||||
# Same build plate
|
# Same build plate
|
||||||
build_plate_number = current_node.callDecoration("getBuildPlateNumber")
|
build_plate_number = current_node.callDecoration("getBuildPlateNumber")
|
||||||
node.callDecoration("setBuildPlateNumber", build_plate_number)
|
new_node.callDecoration("setBuildPlateNumber", build_plate_number)
|
||||||
|
for child in new_node.getChildren():
|
||||||
|
child.callDecoration("setBuildPlateNumber", build_plate_number)
|
||||||
|
|
||||||
nodes.append(node)
|
nodes.append(new_node)
|
||||||
current_progress += 1
|
current_progress += 1
|
||||||
status_message.setProgress((current_progress / total_progress) * 100)
|
status_message.setProgress((current_progress / total_progress) * 100)
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QTimer
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
|
@ -14,17 +19,23 @@ class ObjectsModel(ListModel):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
Application.getInstance().getController().getScene().sceneChanged.connect(self._update)
|
Application.getInstance().getController().getScene().sceneChanged.connect(self._updateDelayed)
|
||||||
Preferences.getInstance().preferenceChanged.connect(self._update)
|
Preferences.getInstance().preferenceChanged.connect(self._updateDelayed)
|
||||||
|
|
||||||
|
self._update_timer = QTimer()
|
||||||
|
self._update_timer.setInterval(100)
|
||||||
|
self._update_timer.setSingleShot(True)
|
||||||
|
self._update_timer.timeout.connect(self._update)
|
||||||
|
|
||||||
self._build_plate_number = -1
|
self._build_plate_number = -1
|
||||||
|
|
||||||
self._stacks_have_errors = None # type:Optional[bool]
|
|
||||||
|
|
||||||
def setActiveBuildPlate(self, nr):
|
def setActiveBuildPlate(self, nr):
|
||||||
self._build_plate_number = nr
|
self._build_plate_number = nr
|
||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
|
def _updateDelayed(self, *args):
|
||||||
|
self._update_timer.start()
|
||||||
|
|
||||||
def _update(self, *args):
|
def _update(self, *args):
|
||||||
nodes = []
|
nodes = []
|
||||||
filter_current_build_plate = Preferences.getInstance().getValue("view/filter_current_build_plate")
|
filter_current_build_plate = Preferences.getInstance().getValue("view/filter_current_build_plate")
|
||||||
|
@ -69,11 +80,3 @@ class ObjectsModel(ListModel):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def createObjectsModel():
|
def createObjectsModel():
|
||||||
return ObjectsModel()
|
return ObjectsModel()
|
||||||
|
|
||||||
## Check if none of the model's stacks contain error states
|
|
||||||
# The setting applied for the settings per model
|
|
||||||
def stacksHaveErrors(self) -> bool:
|
|
||||||
return bool(self._stacks_have_errors)
|
|
||||||
|
|
||||||
def setStacksHaveErrors(self, value):
|
|
||||||
self._stacks_have_errors = value
|
|
69
cura/PickingPass.py
Normal file
69
cura/PickingPass.py
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
# 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.Vector import Vector
|
||||||
|
from UM.Resources import Resources
|
||||||
|
|
||||||
|
from UM.View.RenderPass import RenderPass
|
||||||
|
from UM.View.GL.OpenGL import OpenGL
|
||||||
|
from UM.View.RenderBatch import RenderBatch
|
||||||
|
|
||||||
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
|
|
||||||
|
|
||||||
|
## A RenderPass subclass that renders a the distance of selectable objects from the active camera to a texture.
|
||||||
|
# The texture is used to map a 2d location (eg the mouse location) to a world space position
|
||||||
|
#
|
||||||
|
# Note that in order to increase precision, the 24 bit depth value is encoded into all three of the R,G & B channels
|
||||||
|
class PickingPass(RenderPass):
|
||||||
|
def __init__(self, width: int, height: int):
|
||||||
|
super().__init__("picking", width, height)
|
||||||
|
|
||||||
|
self._renderer = Application.getInstance().getRenderer()
|
||||||
|
|
||||||
|
self._shader = None
|
||||||
|
self._scene = Application.getInstance().getController().getScene()
|
||||||
|
|
||||||
|
def render(self) -> None:
|
||||||
|
if not self._shader:
|
||||||
|
self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "camera_distance.shader"))
|
||||||
|
|
||||||
|
width, height = self.getSize()
|
||||||
|
self._gl.glViewport(0, 0, width, height)
|
||||||
|
self._gl.glClearColor(1.0, 1.0, 1.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)
|
||||||
|
|
||||||
|
# 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())
|
||||||
|
|
||||||
|
self.bind()
|
||||||
|
batch.render(self._scene.getActiveCamera())
|
||||||
|
self.release()
|
||||||
|
|
||||||
|
## Get the distance in mm from the camera to at a certain pixel coordinate.
|
||||||
|
def getPickedDepth(self, x: int, y: int) -> float:
|
||||||
|
output = self.getOutput()
|
||||||
|
|
||||||
|
window_size = self._renderer.getWindowSize()
|
||||||
|
|
||||||
|
px = (0.5 + x / 2.0) * window_size[0]
|
||||||
|
py = (0.5 + y / 2.0) * window_size[1]
|
||||||
|
|
||||||
|
if px < 0 or px > (output.width() - 1) or py < 0 or py > (output.height() - 1):
|
||||||
|
return -1
|
||||||
|
|
||||||
|
distance = output.pixel(px, py) # distance in micron, from in r, g & b channels
|
||||||
|
distance = (distance & 0x00ffffff) / 1000. # drop the alpha channel and covert to mm
|
||||||
|
return distance
|
||||||
|
|
||||||
|
## Get the world coordinates of a picked point
|
||||||
|
def getPickedPosition(self, x: int, y: int) -> Vector:
|
||||||
|
distance = self.getPickedDepth(x, y)
|
||||||
|
ray = self._scene.getActiveCamera().getRay(x, y)
|
||||||
|
|
||||||
|
return ray.getPointAlongRay(distance)
|
|
@ -40,6 +40,8 @@ class PlatformPhysics:
|
||||||
Preferences.getInstance().addPreference("physics/automatic_drop_down", True)
|
Preferences.getInstance().addPreference("physics/automatic_drop_down", True)
|
||||||
|
|
||||||
def _onSceneChanged(self, source):
|
def _onSceneChanged(self, source):
|
||||||
|
if not source.getMeshData():
|
||||||
|
return
|
||||||
self._change_timer.start()
|
self._change_timer.start()
|
||||||
|
|
||||||
def _onChangeTimerFinished(self):
|
def _onChangeTimerFinished(self):
|
||||||
|
@ -69,7 +71,7 @@ class PlatformPhysics:
|
||||||
# Move it downwards if bottom is above platform
|
# Move it downwards if bottom is above platform
|
||||||
move_vector = Vector()
|
move_vector = Vector()
|
||||||
|
|
||||||
if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup")) and node.isEnabled(): #If an object is grouped, don't move it down
|
if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root) and node.isEnabled(): #If an object is grouped, don't move it down
|
||||||
z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
|
z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
|
||||||
move_vector = move_vector.set(y = -bbox.bottom + z_offset)
|
move_vector = move_vector.set(y = -bbox.bottom + z_offset)
|
||||||
|
|
||||||
|
|
|
@ -309,16 +309,13 @@ class PrintInformation(QObject):
|
||||||
|
|
||||||
self.jobNameChanged.emit()
|
self.jobNameChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(str)
|
|
||||||
def baseName(self):
|
|
||||||
return self._base_name
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def setProjectName(self, name):
|
def setProjectName(self, name):
|
||||||
self.setBaseName(name, is_project_file = True)
|
self.setBaseName(name, is_project_file = True)
|
||||||
|
|
||||||
@pyqtSlot(str)
|
baseNameChanged = pyqtSignal()
|
||||||
def setBaseName(self, base_name, is_project_file = False):
|
|
||||||
|
def setBaseName(self, base_name: str, is_project_file: bool = False):
|
||||||
# Ensure that we don't use entire path but only filename
|
# Ensure that we don't use entire path but only filename
|
||||||
name = os.path.basename(base_name)
|
name = os.path.basename(base_name)
|
||||||
|
|
||||||
|
@ -336,6 +333,9 @@ class PrintInformation(QObject):
|
||||||
self._base_name = name
|
self._base_name = name
|
||||||
self._updateJobName()
|
self._updateJobName()
|
||||||
|
|
||||||
|
@pyqtProperty(str, fset = setBaseName, notify = baseNameChanged)
|
||||||
|
def baseName(self):
|
||||||
|
return self._base_name
|
||||||
|
|
||||||
## Created an acronymn-like abbreviated machine name from the currently active machine name
|
## Created an acronymn-like abbreviated machine name from the currently active machine name
|
||||||
# Called each time the global stack is switched
|
# Called each time the global stack is switched
|
||||||
|
@ -395,7 +395,6 @@ class PrintInformation(QObject):
|
||||||
|
|
||||||
## Listen to scene changes to check if we need to reset the print information
|
## Listen to scene changes to check if we need to reset the print information
|
||||||
def _onSceneChanged(self, scene_node):
|
def _onSceneChanged(self, scene_node):
|
||||||
|
|
||||||
# Ignore any changes that are not related to sliceable objects
|
# Ignore any changes that are not related to sliceable objects
|
||||||
if not isinstance(scene_node, SceneNode)\
|
if not isinstance(scene_node, SceneNode)\
|
||||||
or not scene_node.callDecoration("isSliceable")\
|
or not scene_node.callDecoration("isSliceable")\
|
||||||
|
|
81
cura/PrinterOutput/ConfigurationModel.py
Normal file
81
cura/PrinterOutput/ConfigurationModel.py
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
MYPY = False
|
||||||
|
if MYPY:
|
||||||
|
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigurationModel(QObject):
|
||||||
|
|
||||||
|
configurationChanged = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._printer_type = None
|
||||||
|
self._extruder_configurations = [] # type: List[ExtruderConfigurationModel]
|
||||||
|
self._buildplate_configuration = None
|
||||||
|
|
||||||
|
def setPrinterType(self, printer_type):
|
||||||
|
self._printer_type = printer_type
|
||||||
|
|
||||||
|
@pyqtProperty(str, fset = setPrinterType, notify = configurationChanged)
|
||||||
|
def printerType(self):
|
||||||
|
return self._printer_type
|
||||||
|
|
||||||
|
def setExtruderConfigurations(self, extruder_configurations):
|
||||||
|
self._extruder_configurations = extruder_configurations
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantList", fset = setExtruderConfigurations, notify = configurationChanged)
|
||||||
|
def extruderConfigurations(self):
|
||||||
|
return self._extruder_configurations
|
||||||
|
|
||||||
|
def setBuildplateConfiguration(self, buildplate_configuration):
|
||||||
|
self._buildplate_configuration = buildplate_configuration
|
||||||
|
|
||||||
|
@pyqtProperty(str, fset = setBuildplateConfiguration, notify = configurationChanged)
|
||||||
|
def buildplateConfiguration(self):
|
||||||
|
return self._buildplate_configuration
|
||||||
|
|
||||||
|
## This method is intended to indicate whether the configuration is valid or not.
|
||||||
|
# The method checks if the mandatory fields are or not set
|
||||||
|
def isValid(self):
|
||||||
|
if not self._extruder_configurations:
|
||||||
|
return False
|
||||||
|
for configuration in self._extruder_configurations:
|
||||||
|
if configuration is None:
|
||||||
|
return False
|
||||||
|
return self._printer_type is not None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
message_chunks = []
|
||||||
|
message_chunks.append("Printer type: " + self._printer_type)
|
||||||
|
message_chunks.append("Extruders: [")
|
||||||
|
for configuration in self._extruder_configurations:
|
||||||
|
message_chunks.append(" " + str(configuration))
|
||||||
|
message_chunks.append("]")
|
||||||
|
if self._buildplate_configuration is not None:
|
||||||
|
message_chunks.append("Buildplate: " + self._buildplate_configuration)
|
||||||
|
|
||||||
|
return "\n".join(message_chunks)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return hash(self) == hash(other)
|
||||||
|
|
||||||
|
## The hash function is used to compare and create unique sets. The configuration is unique if the configuration
|
||||||
|
# of the extruders is unique (the order of the extruders matters), and the type and buildplate is the same.
|
||||||
|
def __hash__(self):
|
||||||
|
extruder_hash = hash(0)
|
||||||
|
first_extruder = None
|
||||||
|
for configuration in self._extruder_configurations:
|
||||||
|
extruder_hash ^= hash(configuration)
|
||||||
|
if configuration.position == 0:
|
||||||
|
first_extruder = configuration
|
||||||
|
# To ensure the correct order of the extruders, we add an "and" operation using the first extruder hash value
|
||||||
|
if first_extruder:
|
||||||
|
extruder_hash &= hash(first_extruder)
|
||||||
|
|
||||||
|
return hash(self._printer_type) ^ extruder_hash ^ hash(self._buildplate_configuration)
|
59
cura/PrinterOutput/ExtruderConfigurationModel.py
Normal file
59
cura/PrinterOutput/ExtruderConfigurationModel.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal
|
||||||
|
|
||||||
|
|
||||||
|
class ExtruderConfigurationModel(QObject):
|
||||||
|
|
||||||
|
extruderConfigurationChanged = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._position = -1
|
||||||
|
self._material = None
|
||||||
|
self._hotend_id = None
|
||||||
|
|
||||||
|
def setPosition(self, position):
|
||||||
|
self._position = position
|
||||||
|
|
||||||
|
@pyqtProperty(int, fset = setPosition, notify = extruderConfigurationChanged)
|
||||||
|
def position(self):
|
||||||
|
return self._position
|
||||||
|
|
||||||
|
def setMaterial(self, material):
|
||||||
|
self._material = material
|
||||||
|
|
||||||
|
@pyqtProperty(QObject, fset = setMaterial, notify = extruderConfigurationChanged)
|
||||||
|
def material(self):
|
||||||
|
return self._material
|
||||||
|
|
||||||
|
def setHotendID(self, hotend_id):
|
||||||
|
self._hotend_id = hotend_id
|
||||||
|
|
||||||
|
@pyqtProperty(str, fset = setHotendID, notify = extruderConfigurationChanged)
|
||||||
|
def hotendID(self):
|
||||||
|
return self._hotend_id
|
||||||
|
|
||||||
|
## This method is intended to indicate whether the configuration is valid or not.
|
||||||
|
# The method checks if the mandatory fields are or not set
|
||||||
|
# At this moment is always valid since we allow to have empty material and variants.
|
||||||
|
def isValid(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
message_chunks = []
|
||||||
|
message_chunks.append("Position: " + str(self._position))
|
||||||
|
message_chunks.append("-")
|
||||||
|
message_chunks.append("Material: " + self.material.type if self.material else "empty")
|
||||||
|
message_chunks.append("-")
|
||||||
|
message_chunks.append("HotendID: " + self.hotendID if self.hotendID else "empty")
|
||||||
|
return " ".join(message_chunks)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return hash(self) == hash(other)
|
||||||
|
|
||||||
|
# Calculating a hash function using the position of the extruder, the material GUID and the hotend id to check if is
|
||||||
|
# unique within a set
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self._position) ^ (hash(self._material.guid) if self._material is not None else hash(0)) ^ hash(self._hotend_id)
|
|
@ -1,8 +1,8 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot
|
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
|
||||||
from UM.Logger import Logger
|
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
@ -17,14 +17,34 @@ class ExtruderOutputModel(QObject):
|
||||||
targetHotendTemperatureChanged = pyqtSignal()
|
targetHotendTemperatureChanged = pyqtSignal()
|
||||||
hotendTemperatureChanged = pyqtSignal()
|
hotendTemperatureChanged = pyqtSignal()
|
||||||
activeMaterialChanged = pyqtSignal()
|
activeMaterialChanged = pyqtSignal()
|
||||||
|
extruderConfigurationChanged = pyqtSignal()
|
||||||
|
isPreheatingChanged = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, printer: "PrinterOutputModel", parent=None):
|
def __init__(self, printer: "PrinterOutputModel", position, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._printer = printer
|
self._printer = printer
|
||||||
|
self._position = position
|
||||||
self._target_hotend_temperature = 0
|
self._target_hotend_temperature = 0
|
||||||
self._hotend_temperature = 0
|
self._hotend_temperature = 0
|
||||||
self._hotend_id = ""
|
self._hotend_id = ""
|
||||||
self._active_material = None # type: Optional[MaterialOutputModel]
|
self._active_material = None # type: Optional[MaterialOutputModel]
|
||||||
|
self._extruder_configuration = ExtruderConfigurationModel()
|
||||||
|
self._extruder_configuration.position = self._position
|
||||||
|
|
||||||
|
self._is_preheating = False
|
||||||
|
|
||||||
|
def getPrinter(self):
|
||||||
|
return self._printer
|
||||||
|
|
||||||
|
def getPosition(self):
|
||||||
|
return self._position
|
||||||
|
|
||||||
|
# Does the printer support pre-heating the bed at all
|
||||||
|
@pyqtProperty(bool, constant=True)
|
||||||
|
def canPreHeatHotends(self):
|
||||||
|
if self._printer:
|
||||||
|
return self._printer.canPreHeatHotends
|
||||||
|
return False
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify = activeMaterialChanged)
|
@pyqtProperty(QObject, notify = activeMaterialChanged)
|
||||||
def activeMaterial(self) -> "MaterialOutputModel":
|
def activeMaterial(self) -> "MaterialOutputModel":
|
||||||
|
@ -33,7 +53,9 @@ class ExtruderOutputModel(QObject):
|
||||||
def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]):
|
def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]):
|
||||||
if self._active_material != material:
|
if self._active_material != material:
|
||||||
self._active_material = material
|
self._active_material = material
|
||||||
|
self._extruder_configuration.material = self._active_material
|
||||||
self.activeMaterialChanged.emit()
|
self.activeMaterialChanged.emit()
|
||||||
|
self.extruderConfigurationChanged.emit()
|
||||||
|
|
||||||
## Update the hotend temperature. This only changes it locally.
|
## Update the hotend temperature. This only changes it locally.
|
||||||
def updateHotendTemperature(self, temperature: float):
|
def updateHotendTemperature(self, temperature: float):
|
||||||
|
@ -67,4 +89,34 @@ class ExtruderOutputModel(QObject):
|
||||||
def updateHotendID(self, id: str):
|
def updateHotendID(self, id: str):
|
||||||
if self._hotend_id != id:
|
if self._hotend_id != id:
|
||||||
self._hotend_id = id
|
self._hotend_id = id
|
||||||
|
self._extruder_configuration.hotendID = self._hotend_id
|
||||||
self.hotendIDChanged.emit()
|
self.hotendIDChanged.emit()
|
||||||
|
self.extruderConfigurationChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(QObject, notify = extruderConfigurationChanged)
|
||||||
|
def extruderConfiguration(self):
|
||||||
|
if self._extruder_configuration.isValid():
|
||||||
|
return self._extruder_configuration
|
||||||
|
return None
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
## Pre-heats the extruder before printer.
|
||||||
|
#
|
||||||
|
# \param temperature The temperature to heat the extruder to, in degrees
|
||||||
|
# Celsius.
|
||||||
|
# \param duration How long the bed should stay warm, in seconds.
|
||||||
|
@pyqtSlot(float, float)
|
||||||
|
def preheatHotend(self, temperature, duration):
|
||||||
|
self._printer._controller.preheatHotend(self, temperature, duration)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def cancelPreheatHotend(self):
|
||||||
|
self._printer._controller.cancelPreheatHotend(self)
|
154
cura/PrinterOutput/GenericOutputController.py
Normal file
154
cura/PrinterOutput/GenericOutputController.py
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||||
|
from PyQt5.QtCore import QTimer
|
||||||
|
|
||||||
|
MYPY = False
|
||||||
|
if MYPY:
|
||||||
|
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
||||||
|
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||||
|
|
||||||
|
|
||||||
|
class GenericOutputController(PrinterOutputController):
|
||||||
|
def __init__(self, output_device):
|
||||||
|
super().__init__(output_device)
|
||||||
|
|
||||||
|
self._preheat_bed_timer = QTimer()
|
||||||
|
self._preheat_bed_timer.setSingleShot(True)
|
||||||
|
self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished)
|
||||||
|
self._preheat_printer = None
|
||||||
|
|
||||||
|
self._preheat_hotends_timer = QTimer()
|
||||||
|
self._preheat_hotends_timer.setSingleShot(True)
|
||||||
|
self._preheat_hotends_timer.timeout.connect(self._onPreheatHotendsTimerFinished)
|
||||||
|
self._preheat_hotends = set()
|
||||||
|
|
||||||
|
self._output_device.printersChanged.connect(self._onPrintersChanged)
|
||||||
|
self._active_printer = None
|
||||||
|
|
||||||
|
def _onPrintersChanged(self):
|
||||||
|
if self._active_printer:
|
||||||
|
self._active_printer.stateChanged.disconnect(self._onPrinterStateChanged)
|
||||||
|
self._active_printer.targetBedTemperatureChanged.disconnect(self._onTargetBedTemperatureChanged)
|
||||||
|
for extruder in self._active_printer.extruders:
|
||||||
|
extruder.targetHotendTemperatureChanged.disconnect(self._onTargetHotendTemperatureChanged)
|
||||||
|
|
||||||
|
self._active_printer = self._output_device.activePrinter
|
||||||
|
if self._active_printer:
|
||||||
|
self._active_printer.stateChanged.connect(self._onPrinterStateChanged)
|
||||||
|
self._active_printer.targetBedTemperatureChanged.connect(self._onTargetBedTemperatureChanged)
|
||||||
|
for extruder in self._active_printer.extruders:
|
||||||
|
extruder.targetHotendTemperatureChanged.connect(self._onTargetHotendTemperatureChanged)
|
||||||
|
|
||||||
|
def _onPrinterStateChanged(self):
|
||||||
|
if self._active_printer.state != "idle":
|
||||||
|
if self._preheat_bed_timer.isActive():
|
||||||
|
self._preheat_bed_timer.stop()
|
||||||
|
self._preheat_printer.updateIsPreheating(False)
|
||||||
|
if self._preheat_hotends_timer.isActive():
|
||||||
|
self._preheat_hotends_timer.stop()
|
||||||
|
for extruder in self._preheat_hotends:
|
||||||
|
extruder.updateIsPreheating(False)
|
||||||
|
self._preheat_hotends = set()
|
||||||
|
|
||||||
|
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed):
|
||||||
|
self._output_device.sendCommand("G91")
|
||||||
|
self._output_device.sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))
|
||||||
|
self._output_device.sendCommand("G90")
|
||||||
|
|
||||||
|
def homeHead(self, printer):
|
||||||
|
self._output_device.sendCommand("G28 X")
|
||||||
|
self._output_device.sendCommand("G28 Y")
|
||||||
|
|
||||||
|
def homeBed(self, printer):
|
||||||
|
self._output_device.sendCommand("G28 Z")
|
||||||
|
|
||||||
|
def sendRawCommand(self, printer: "PrinterOutputModel", command: str):
|
||||||
|
self._output_device.sendCommand(command)
|
||||||
|
|
||||||
|
def setJobState(self, job: "PrintJobOutputModel", state: str):
|
||||||
|
if state == "pause":
|
||||||
|
self._output_device.pausePrint()
|
||||||
|
job.updateState("paused")
|
||||||
|
elif state == "print":
|
||||||
|
self._output_device.resumePrint()
|
||||||
|
job.updateState("printing")
|
||||||
|
elif state == "abort":
|
||||||
|
self._output_device.cancelPrint()
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int):
|
||||||
|
self._output_device.sendCommand("M140 S%s" % temperature)
|
||||||
|
|
||||||
|
def _onTargetBedTemperatureChanged(self):
|
||||||
|
if self._preheat_bed_timer.isActive() and self._preheat_printer.targetBedTemperature == 0:
|
||||||
|
self._preheat_bed_timer.stop()
|
||||||
|
self._preheat_printer.updateIsPreheating(False)
|
||||||
|
|
||||||
|
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration):
|
||||||
|
try:
|
||||||
|
temperature = round(temperature) # The API doesn't allow floating point.
|
||||||
|
duration = round(duration)
|
||||||
|
except ValueError:
|
||||||
|
return # Got invalid values, can't pre-heat.
|
||||||
|
|
||||||
|
self.setTargetBedTemperature(printer, temperature=temperature)
|
||||||
|
self._preheat_bed_timer.setInterval(duration * 1000)
|
||||||
|
self._preheat_bed_timer.start()
|
||||||
|
self._preheat_printer = printer
|
||||||
|
printer.updateIsPreheating(True)
|
||||||
|
|
||||||
|
def cancelPreheatBed(self, printer: "PrinterOutputModel"):
|
||||||
|
self.setTargetBedTemperature(printer, temperature=0)
|
||||||
|
self._preheat_bed_timer.stop()
|
||||||
|
printer.updateIsPreheating(False)
|
||||||
|
|
||||||
|
def _onPreheatBedTimerFinished(self):
|
||||||
|
self.setTargetBedTemperature(self._preheat_printer, 0)
|
||||||
|
self._preheat_printer.updateIsPreheating(False)
|
||||||
|
|
||||||
|
def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: int):
|
||||||
|
self._output_device.sendCommand("M104 S%s T%s" % (temperature, position))
|
||||||
|
|
||||||
|
def _onTargetHotendTemperatureChanged(self):
|
||||||
|
if not self._preheat_hotends_timer.isActive():
|
||||||
|
return
|
||||||
|
|
||||||
|
for extruder in self._active_printer.extruders:
|
||||||
|
if extruder in self._preheat_hotends and extruder.targetHotendTemperature == 0:
|
||||||
|
extruder.updateIsPreheating(False)
|
||||||
|
self._preheat_hotends.remove(extruder)
|
||||||
|
if not self._preheat_hotends:
|
||||||
|
self._preheat_hotends_timer.stop()
|
||||||
|
|
||||||
|
def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration):
|
||||||
|
position = extruder.getPosition()
|
||||||
|
number_of_extruders = len(extruder.getPrinter().extruders)
|
||||||
|
if position >= number_of_extruders:
|
||||||
|
return # Got invalid extruder nr, can't pre-heat.
|
||||||
|
|
||||||
|
try:
|
||||||
|
temperature = round(temperature) # The API doesn't allow floating point.
|
||||||
|
duration = round(duration)
|
||||||
|
except ValueError:
|
||||||
|
return # Got invalid values, can't pre-heat.
|
||||||
|
|
||||||
|
self.setTargetHotendTemperature(extruder.getPrinter(), position, temperature=temperature)
|
||||||
|
self._preheat_hotends_timer.setInterval(duration * 1000)
|
||||||
|
self._preheat_hotends_timer.start()
|
||||||
|
self._preheat_hotends.add(extruder)
|
||||||
|
extruder.updateIsPreheating(True)
|
||||||
|
|
||||||
|
def cancelPreheatHotend(self, extruder: "ExtruderOutputModel"):
|
||||||
|
self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), temperature=0)
|
||||||
|
if extruder in self._preheat_hotends:
|
||||||
|
extruder.updateIsPreheating(False)
|
||||||
|
self._preheat_hotends.remove(extruder)
|
||||||
|
if not self._preheat_hotends and self._preheat_hotends_timer.isActive():
|
||||||
|
self._preheat_hotends_timer.stop()
|
||||||
|
|
||||||
|
def _onPreheatHotendsTimerFinished(self):
|
||||||
|
for extruder in self._preheat_hotends:
|
||||||
|
self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), 0)
|
||||||
|
self._preheat_hotends = set()
|
|
@ -1,8 +1,9 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
|
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
|
||||||
|
|
||||||
|
@ -55,6 +56,17 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
|
|
||||||
self._connection_state_before_timeout = None # type: Optional[ConnectionState]
|
self._connection_state_before_timeout = None # type: Optional[ConnectionState]
|
||||||
|
|
||||||
|
printer_type = self._properties.get(b"machine", b"").decode("utf-8")
|
||||||
|
printer_type_identifiers = {
|
||||||
|
"9066": "ultimaker3",
|
||||||
|
"9511": "ultimaker3_extended"
|
||||||
|
}
|
||||||
|
self._printer_type = "Unknown"
|
||||||
|
for key, value in printer_type_identifiers.items():
|
||||||
|
if printer_type.startswith(key):
|
||||||
|
self._printer_type = value
|
||||||
|
break
|
||||||
|
|
||||||
def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs) -> None:
|
def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs) -> None:
|
||||||
raise NotImplementedError("requestWrite needs to be implemented")
|
raise NotImplementedError("requestWrite needs to be implemented")
|
||||||
|
|
||||||
|
@ -219,7 +231,6 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
reply.uploadProgress.connect(onProgress)
|
reply.uploadProgress.connect(onProgress)
|
||||||
self._registerOnFinishedCallback(reply, onFinished)
|
self._registerOnFinishedCallback(reply, onFinished)
|
||||||
|
|
||||||
|
|
||||||
return reply
|
return reply
|
||||||
|
|
||||||
def postForm(self, target: str, header_data: str, body_data: bytes, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None:
|
def postForm(self, target: str, header_data: str, body_data: bytes, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None:
|
||||||
|
@ -243,6 +254,9 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
self._last_manager_create_time = time()
|
self._last_manager_create_time = time()
|
||||||
self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
|
self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
|
||||||
|
|
||||||
|
machine_manager = CuraApplication.getInstance().getMachineManager()
|
||||||
|
machine_manager.checkCorrectGroupName(self.getId(), self.name)
|
||||||
|
|
||||||
def _registerOnFinishedCallback(self, reply: QNetworkReply, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None:
|
def _registerOnFinishedCallback(self, reply: QNetworkReply, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None:
|
||||||
if onFinished is not None:
|
if onFinished is not None:
|
||||||
self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished
|
self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished
|
||||||
|
@ -301,6 +315,10 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
def firmwareVersion(self) -> str:
|
def firmwareVersion(self) -> str:
|
||||||
return self._properties.get(b"firmware_version", b"").decode("utf-8")
|
return self._properties.get(b"firmware_version", b"").decode("utf-8")
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant=True)
|
||||||
|
def printerType(self) -> str:
|
||||||
|
return self._printer_type
|
||||||
|
|
||||||
## IPadress of this printer
|
## IPadress of this printer
|
||||||
@pyqtProperty(str, constant=True)
|
@pyqtProperty(str, constant=True)
|
||||||
def ipAddress(self) -> str:
|
def ipAddress(self) -> str:
|
||||||
|
|
|
@ -6,7 +6,7 @@ from UM.Logger import Logger
|
||||||
MYPY = False
|
MYPY = False
|
||||||
if MYPY:
|
if MYPY:
|
||||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
||||||
from cura.PrinterOutput.ExtruderOuputModel import ExtruderOuputModel
|
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,10 +15,12 @@ class PrinterOutputController:
|
||||||
self.can_pause = True
|
self.can_pause = True
|
||||||
self.can_abort = True
|
self.can_abort = True
|
||||||
self.can_pre_heat_bed = True
|
self.can_pre_heat_bed = True
|
||||||
|
self.can_pre_heat_hotends = True
|
||||||
|
self.can_send_raw_gcode = True
|
||||||
self.can_control_manually = True
|
self.can_control_manually = True
|
||||||
self._output_device = output_device
|
self._output_device = output_device
|
||||||
|
|
||||||
def setTargetHotendTemperature(self, printer: "PrinterOutputModel", extruder: "ExtruderOuputModel", temperature: int):
|
def setTargetHotendTemperature(self, printer: "PrinterOutputModel", extruder: "ExtruderOutputModel", temperature: int):
|
||||||
Logger.log("w", "Set target hotend temperature not implemented in controller")
|
Logger.log("w", "Set target hotend temperature not implemented in controller")
|
||||||
|
|
||||||
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int):
|
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int):
|
||||||
|
@ -33,14 +35,23 @@ class PrinterOutputController:
|
||||||
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration):
|
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration):
|
||||||
Logger.log("w", "Preheat bed not implemented in controller")
|
Logger.log("w", "Preheat bed not implemented in controller")
|
||||||
|
|
||||||
|
def cancelPreheatHotend(self, extruder: "ExtruderOutputModel"):
|
||||||
|
Logger.log("w", "Cancel preheat hotend not implemented in controller")
|
||||||
|
|
||||||
|
def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration):
|
||||||
|
Logger.log("w", "Preheat hotend not implemented in controller")
|
||||||
|
|
||||||
def setHeadPosition(self, printer: "PrinterOutputModel", x, y, z, speed):
|
def setHeadPosition(self, printer: "PrinterOutputModel", x, y, z, speed):
|
||||||
Logger.log("w", "Set head position not implemented in controller")
|
Logger.log("w", "Set head position not implemented in controller")
|
||||||
|
|
||||||
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed):
|
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed):
|
||||||
Logger.log("w", "Move head not implemented in controller")
|
Logger.log("w", "Move head not implemented in controller")
|
||||||
|
|
||||||
def homeBed(self, printer):
|
def homeBed(self, printer: "PrinterOutputModel"):
|
||||||
Logger.log("w", "Home bed not implemented in controller")
|
Logger.log("w", "Home bed not implemented in controller")
|
||||||
|
|
||||||
def homeHead(self, printer):
|
def homeHead(self, printer: "PrinterOutputModel"):
|
||||||
Logger.log("w", "Home head not implemented in controller")
|
Logger.log("w", "Home head not implemented in controller")
|
||||||
|
|
||||||
|
def sendRawCommand(self, printer: "PrinterOutputModel", command: str):
|
||||||
|
Logger.log("w", "Custom command not implemented in controller")
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot
|
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot
|
||||||
from UM.Logger import Logger
|
from typing import Optional
|
||||||
from typing import Optional, List
|
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
from cura.PrinterOutput.ExtruderOuputModel import ExtruderOutputModel
|
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
||||||
|
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
||||||
|
|
||||||
MYPY = False
|
MYPY = False
|
||||||
if MYPY:
|
if MYPY:
|
||||||
|
@ -22,8 +22,10 @@ class PrinterOutputModel(QObject):
|
||||||
nameChanged = pyqtSignal()
|
nameChanged = pyqtSignal()
|
||||||
headPositionChanged = pyqtSignal()
|
headPositionChanged = pyqtSignal()
|
||||||
keyChanged = pyqtSignal()
|
keyChanged = pyqtSignal()
|
||||||
typeChanged = pyqtSignal()
|
printerTypeChanged = pyqtSignal()
|
||||||
|
buildplateChanged = pyqtSignal()
|
||||||
cameraChanged = pyqtSignal()
|
cameraChanged = pyqtSignal()
|
||||||
|
configurationChanged = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = ""):
|
def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = ""):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
@ -32,13 +34,18 @@ class PrinterOutputModel(QObject):
|
||||||
self._name = ""
|
self._name = ""
|
||||||
self._key = "" # Unique identifier
|
self._key = "" # Unique identifier
|
||||||
self._controller = output_controller
|
self._controller = output_controller
|
||||||
self._extruders = [ExtruderOutputModel(printer=self) for i in range(number_of_extruders)]
|
self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)]
|
||||||
|
self._printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer
|
||||||
self._head_position = Vector(0, 0, 0)
|
self._head_position = Vector(0, 0, 0)
|
||||||
self._active_print_job = None # type: Optional[PrintJobOutputModel]
|
self._active_print_job = None # type: Optional[PrintJobOutputModel]
|
||||||
self._firmware_version = firmware_version
|
self._firmware_version = firmware_version
|
||||||
self._printer_state = "unknown"
|
self._printer_state = "unknown"
|
||||||
self._is_preheating = False
|
self._is_preheating = False
|
||||||
self._type = ""
|
self._printer_type = ""
|
||||||
|
self._buildplate_name = None
|
||||||
|
# Update the printer configuration every time any of the extruders changes its configuration
|
||||||
|
for extruder in self._extruders:
|
||||||
|
extruder.extruderConfigurationChanged.connect(self._updateExtruderConfiguration)
|
||||||
|
|
||||||
self._camera = None
|
self._camera = None
|
||||||
|
|
||||||
|
@ -64,14 +71,27 @@ class PrinterOutputModel(QObject):
|
||||||
def camera(self):
|
def camera(self):
|
||||||
return self._camera
|
return self._camera
|
||||||
|
|
||||||
@pyqtProperty(str, notify = typeChanged)
|
@pyqtProperty(str, notify = printerTypeChanged)
|
||||||
def type(self):
|
def type(self):
|
||||||
return self._type
|
return self._printer_type
|
||||||
|
|
||||||
def updateType(self, type):
|
def updateType(self, printer_type):
|
||||||
if self._type != type:
|
if self._printer_type != printer_type:
|
||||||
self._type = type
|
self._printer_type = printer_type
|
||||||
self.typeChanged.emit()
|
self._printer_configuration.printerType = self._printer_type
|
||||||
|
self.printerTypeChanged.emit()
|
||||||
|
self.configurationChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = buildplateChanged)
|
||||||
|
def buildplate(self):
|
||||||
|
return self._buildplate_name
|
||||||
|
|
||||||
|
def updateBuildplateName(self, buildplate_name):
|
||||||
|
if self._buildplate_name != buildplate_name:
|
||||||
|
self._buildplate_name = buildplate_name
|
||||||
|
self._printer_configuration.buildplateConfiguration = self._buildplate_name
|
||||||
|
self.buildplateChanged.emit()
|
||||||
|
self.configurationChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(str, notify=keyChanged)
|
@pyqtProperty(str, notify=keyChanged)
|
||||||
def key(self):
|
def key(self):
|
||||||
|
@ -90,6 +110,10 @@ class PrinterOutputModel(QObject):
|
||||||
def homeBed(self):
|
def homeBed(self):
|
||||||
self._controller.homeBed(self)
|
self._controller.homeBed(self)
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def sendRawCommand(self, command: str):
|
||||||
|
self._controller.sendRawCommand(self, command)
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", constant = True)
|
@pyqtProperty("QVariantList", constant = True)
|
||||||
def extruders(self):
|
def extruders(self):
|
||||||
return self._extruders
|
return self._extruders
|
||||||
|
@ -103,32 +127,32 @@ class PrinterOutputModel(QObject):
|
||||||
self._head_position = Vector(x, y, z)
|
self._head_position = Vector(x, y, z)
|
||||||
self.headPositionChanged.emit()
|
self.headPositionChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty("long", "long", "long")
|
@pyqtProperty(float, float, float)
|
||||||
@pyqtProperty("long", "long", "long", "long")
|
@pyqtProperty(float, float, float, float)
|
||||||
def setHeadPosition(self, x, y, z, speed = 3000):
|
def setHeadPosition(self, x, y, z, speed = 3000):
|
||||||
self.updateHeadPosition(x, y, z)
|
self.updateHeadPosition(x, y, z)
|
||||||
self._controller.setHeadPosition(self, x, y, z, speed)
|
self._controller.setHeadPosition(self, x, y, z, speed)
|
||||||
|
|
||||||
@pyqtProperty("long")
|
@pyqtProperty(float)
|
||||||
@pyqtProperty("long", "long")
|
@pyqtProperty(float, float)
|
||||||
def setHeadX(self, x, speed = 3000):
|
def setHeadX(self, x, speed = 3000):
|
||||||
self.updateHeadPosition(x, self._head_position.y, self._head_position.z)
|
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)
|
self._controller.setHeadPosition(self, x, self._head_position.y, self._head_position.z, speed)
|
||||||
|
|
||||||
@pyqtProperty("long")
|
@pyqtProperty(float)
|
||||||
@pyqtProperty("long", "long")
|
@pyqtProperty(float, float)
|
||||||
def setHeadY(self, y, speed = 3000):
|
def setHeadY(self, y, speed = 3000):
|
||||||
self.updateHeadPosition(self._head_position.x, y, self._head_position.z)
|
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)
|
self._controller.setHeadPosition(self, self._head_position.x, y, self._head_position.z, speed)
|
||||||
|
|
||||||
@pyqtProperty("long")
|
@pyqtProperty(float)
|
||||||
@pyqtProperty("long", "long")
|
@pyqtProperty(float, float)
|
||||||
def setHeadZ(self, z, speed = 3000):
|
def setHeadZ(self, z, speed = 3000):
|
||||||
self.updateHeadPosition(self._head_position.x, self._head_position.y, z)
|
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)
|
self._controller.setHeadPosition(self, self._head_position.x, self._head_position.y, z, speed)
|
||||||
|
|
||||||
@pyqtSlot("long", "long", "long")
|
@pyqtSlot(float, float, float)
|
||||||
@pyqtSlot("long", "long", "long", "long")
|
@pyqtSlot(float, float, float, float)
|
||||||
def moveHead(self, x = 0, y = 0, z = 0, speed = 3000):
|
def moveHead(self, x = 0, y = 0, z = 0, speed = 3000):
|
||||||
self._controller.moveHead(self, x, y, z, speed)
|
self._controller.moveHead(self, x, y, z, speed)
|
||||||
|
|
||||||
|
@ -218,6 +242,20 @@ class PrinterOutputModel(QObject):
|
||||||
return self._controller.can_pre_heat_bed
|
return self._controller.can_pre_heat_bed
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Does the printer support pre-heating the bed at all
|
||||||
|
@pyqtProperty(bool, constant=True)
|
||||||
|
def canPreHeatHotends(self):
|
||||||
|
if self._controller:
|
||||||
|
return self._controller.can_pre_heat_hotends
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Does the printer support sending raw G-code at all
|
||||||
|
@pyqtProperty(bool, constant=True)
|
||||||
|
def canSendRawGcode(self):
|
||||||
|
if self._controller:
|
||||||
|
return self._controller.can_send_raw_gcode
|
||||||
|
return False
|
||||||
|
|
||||||
# Does the printer support pause at all
|
# Does the printer support pause at all
|
||||||
@pyqtProperty(bool, constant=True)
|
@pyqtProperty(bool, constant=True)
|
||||||
def canPause(self):
|
def canPause(self):
|
||||||
|
@ -238,3 +276,14 @@ class PrinterOutputModel(QObject):
|
||||||
if self._controller:
|
if self._controller:
|
||||||
return self._controller.can_control_manually
|
return self._controller.can_control_manually
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Returns the configuration (material, variant and buildplate) of the current printer
|
||||||
|
@pyqtProperty(QObject, notify = configurationChanged)
|
||||||
|
def printerConfiguration(self):
|
||||||
|
if self._printer_configuration.isValid():
|
||||||
|
return self._printer_configuration
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _updateExtruderConfiguration(self):
|
||||||
|
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in self._extruders]
|
||||||
|
self.configurationChanged.emit()
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.OutputDevice.OutputDevice import OutputDevice
|
from UM.OutputDevice.OutputDevice import OutputDevice
|
||||||
from PyQt5.QtCore import pyqtProperty, QObject, QTimer, pyqtSignal
|
from PyQt5.QtCore import pyqtProperty, QObject, QTimer, pyqtSignal, QVariant
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Signal import signalemitter
|
from UM.Signal import signalemitter
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
@ -17,6 +16,7 @@ from typing import List, Optional
|
||||||
MYPY = False
|
MYPY = False
|
||||||
if MYPY:
|
if MYPY:
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||||
|
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
@ -44,10 +44,14 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
# Signal to indicate that the info text about the connection has changed.
|
# Signal to indicate that the info text about the connection has changed.
|
||||||
connectionTextChanged = pyqtSignal()
|
connectionTextChanged = pyqtSignal()
|
||||||
|
|
||||||
|
# Signal to indicate that the configuration of one of the printers has changed.
|
||||||
|
uniqueConfigurationsChanged = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, device_id, parent = None):
|
def __init__(self, device_id, parent = None):
|
||||||
super().__init__(device_id = device_id, parent = parent)
|
super().__init__(device_id = device_id, parent = parent)
|
||||||
|
|
||||||
self._printers = [] # type: List[PrinterOutputModel]
|
self._printers = [] # type: List[PrinterOutputModel]
|
||||||
|
self._unique_configurations = [] # type: List[ConfigurationModel]
|
||||||
|
|
||||||
self._monitor_view_qml_path = ""
|
self._monitor_view_qml_path = ""
|
||||||
self._monitor_component = None
|
self._monitor_component = None
|
||||||
|
@ -69,6 +73,8 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
|
|
||||||
self._address = ""
|
self._address = ""
|
||||||
self._connection_text = ""
|
self._connection_text = ""
|
||||||
|
self.printersChanged.connect(self._onPrintersChanged)
|
||||||
|
Application.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations)
|
||||||
|
|
||||||
@pyqtProperty(str, notify = connectionTextChanged)
|
@pyqtProperty(str, notify = connectionTextChanged)
|
||||||
def address(self):
|
def address(self):
|
||||||
|
@ -175,6 +181,23 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
|
|
||||||
self.acceptsCommandsChanged.emit()
|
self.acceptsCommandsChanged.emit()
|
||||||
|
|
||||||
|
# Returns the unique configurations of the printers within this output device
|
||||||
|
@pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged)
|
||||||
|
def uniqueConfigurations(self):
|
||||||
|
return self._unique_configurations
|
||||||
|
|
||||||
|
def _updateUniqueConfigurations(self):
|
||||||
|
self._unique_configurations = list(set([printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None]))
|
||||||
|
self._unique_configurations.sort(key = lambda k: k.printerType)
|
||||||
|
self.uniqueConfigurationsChanged.emit()
|
||||||
|
|
||||||
|
def _onPrintersChanged(self):
|
||||||
|
for printer in self._printers:
|
||||||
|
printer.configurationChanged.connect(self._updateUniqueConfigurations)
|
||||||
|
|
||||||
|
# At this point there may be non-updated configurations
|
||||||
|
self._updateUniqueConfigurations()
|
||||||
|
|
||||||
|
|
||||||
## The current processing state of the backend.
|
## The current processing state of the backend.
|
||||||
class ConnectionState(IntEnum):
|
class ConnectionState(IntEnum):
|
||||||
|
|
|
@ -3,6 +3,13 @@
|
||||||
|
|
||||||
from UM.PluginObject import PluginObject
|
from UM.PluginObject import PluginObject
|
||||||
|
|
||||||
|
|
||||||
|
# Exception when there is no profile to import from a given files.
|
||||||
|
# Note that this should not be treated as an exception but as an information instead.
|
||||||
|
class NoProfileException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
## A type of plug-ins that reads profiles from a file.
|
## A type of plug-ins that reads profiles from a file.
|
||||||
#
|
#
|
||||||
# The profile is then stored as instance container of the type user profile.
|
# The profile is then stored as instance container of the type user profile.
|
||||||
|
|
|
@ -15,7 +15,7 @@ class BuildPlateDecorator(SceneNodeDecorator):
|
||||||
self._build_plate_number = nr
|
self._build_plate_number = nr
|
||||||
if isinstance(self._node, CuraSceneNode):
|
if isinstance(self._node, CuraSceneNode):
|
||||||
self._node.transformChanged() # trigger refresh node without introducing a new signal
|
self._node.transformChanged() # trigger refresh node without introducing a new signal
|
||||||
if self._node and self._node.callDecoration("isGroup"):
|
if self._node:
|
||||||
for child in self._node.getChildren():
|
for child in self._node.getChildren():
|
||||||
child.callDecoration("setBuildPlateNumber", nr)
|
child.callDecoration("setBuildPlateNumber", nr)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QTimer
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Math.Polygon import Polygon
|
from UM.Math.Polygon import Polygon
|
||||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||||
|
@ -22,6 +24,10 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
|
|
||||||
self._global_stack = None
|
self._global_stack = None
|
||||||
|
|
||||||
|
# Make sure the timer is created on the main thread
|
||||||
|
self._recompute_convex_hull_timer = None
|
||||||
|
Application.getInstance().callLater(self.createRecomputeConvexHullTimer)
|
||||||
|
|
||||||
self._raft_thickness = 0.0
|
self._raft_thickness = 0.0
|
||||||
# For raft thickness, DRY
|
# For raft thickness, DRY
|
||||||
self._build_volume = Application.getInstance().getBuildVolume()
|
self._build_volume = Application.getInstance().getBuildVolume()
|
||||||
|
@ -33,6 +39,12 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
|
|
||||||
self._onGlobalStackChanged()
|
self._onGlobalStackChanged()
|
||||||
|
|
||||||
|
def createRecomputeConvexHullTimer(self):
|
||||||
|
self._recompute_convex_hull_timer = QTimer()
|
||||||
|
self._recompute_convex_hull_timer.setInterval(200)
|
||||||
|
self._recompute_convex_hull_timer.setSingleShot(True)
|
||||||
|
self._recompute_convex_hull_timer.timeout.connect(self.recomputeConvexHull)
|
||||||
|
|
||||||
def setNode(self, node):
|
def setNode(self, node):
|
||||||
previous_node = self._node
|
previous_node = self._node
|
||||||
# Disconnect from previous node signals
|
# Disconnect from previous node signals
|
||||||
|
@ -99,6 +111,12 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
return self._compute2DConvexHull()
|
return self._compute2DConvexHull()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def recomputeConvexHullDelayed(self):
|
||||||
|
if self._recompute_convex_hull_timer is not None:
|
||||||
|
self._recompute_convex_hull_timer.start()
|
||||||
|
else:
|
||||||
|
self.recomputeConvexHull()
|
||||||
|
|
||||||
def recomputeConvexHull(self):
|
def recomputeConvexHull(self):
|
||||||
controller = Application.getInstance().getController()
|
controller = Application.getInstance().getController()
|
||||||
root = controller.getScene().getRoot()
|
root = controller.getScene().getRoot()
|
||||||
|
@ -279,7 +297,8 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
|
|
||||||
def _onChanged(self, *args):
|
def _onChanged(self, *args):
|
||||||
self._raft_thickness = self._build_volume.getRaftThickness()
|
self._raft_thickness = self._build_volume.getRaftThickness()
|
||||||
self.recomputeConvexHull()
|
if not args or args[0] == self._node:
|
||||||
|
self.recomputeConvexHullDelayed()
|
||||||
|
|
||||||
def _onGlobalStackChanged(self):
|
def _onGlobalStackChanged(self):
|
||||||
if self._global_stack:
|
if self._global_stack:
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from copy import deepcopy
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from copy import deepcopy
|
|
||||||
from cura.Settings.ExtrudersModel import ExtrudersModel
|
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||||
|
|
||||||
|
|
||||||
## Scene nodes that are models are only seen when selecting the corresponding build plate
|
## Scene nodes that are models are only seen when selecting the corresponding build plate
|
||||||
|
@ -11,6 +15,8 @@ from cura.Settings.ExtrudersModel import ExtrudersModel
|
||||||
class CuraSceneNode(SceneNode):
|
class CuraSceneNode(SceneNode):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
if "no_setting_override" not in kwargs:
|
||||||
|
self.addDecorator(SettingOverrideDecorator()) # now we always have a getActiveExtruderPosition, unless explicitly disabled
|
||||||
self._outside_buildarea = False
|
self._outside_buildarea = False
|
||||||
|
|
||||||
def setOutsideBuildArea(self, new_value):
|
def setOutsideBuildArea(self, new_value):
|
||||||
|
@ -35,7 +41,7 @@ class CuraSceneNode(SceneNode):
|
||||||
# Use the support extruder instead of the active extruder if this is a support_mesh
|
# Use the support extruder instead of the active extruder if this is a support_mesh
|
||||||
if per_mesh_stack:
|
if per_mesh_stack:
|
||||||
if per_mesh_stack.getProperty("support_mesh", "value"):
|
if per_mesh_stack.getProperty("support_mesh", "value"):
|
||||||
return extruders[int(global_container_stack.getProperty("support_extruder_nr", "value"))]
|
return extruders[int(global_container_stack.getExtruderPositionValueWithDefault("support_extruder_nr"))]
|
||||||
|
|
||||||
# It's only set if you explicitly choose an extruder
|
# It's only set if you explicitly choose an extruder
|
||||||
extruder_id = self.callDecoration("getActiveExtruder")
|
extruder_id = self.callDecoration("getActiveExtruder")
|
||||||
|
@ -72,9 +78,34 @@ class CuraSceneNode(SceneNode):
|
||||||
1.0
|
1.0
|
||||||
]
|
]
|
||||||
|
|
||||||
|
## Return if the provided bbox collides with the bbox of this scene node
|
||||||
|
def collidesWithBbox(self, check_bbox):
|
||||||
|
bbox = self.getBoundingBox()
|
||||||
|
|
||||||
|
# Mark the node as outside the build volume if the bounding box test fails.
|
||||||
|
if check_bbox.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
## Return if any area collides with the convex hull of this scene node
|
||||||
|
def collidesWithArea(self, areas):
|
||||||
|
convex_hull = self.callDecoration("getConvexHull")
|
||||||
|
if convex_hull:
|
||||||
|
if not convex_hull.isValid():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check for collisions between disallowed areas and the object
|
||||||
|
for area in areas:
|
||||||
|
overlap = convex_hull.intersectsPolygon(area)
|
||||||
|
if overlap is None:
|
||||||
|
continue
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
## Taken from SceneNode, but replaced SceneNode with CuraSceneNode
|
## Taken from SceneNode, but replaced SceneNode with CuraSceneNode
|
||||||
def __deepcopy__(self, memo):
|
def __deepcopy__(self, memo):
|
||||||
copy = CuraSceneNode()
|
copy = CuraSceneNode(no_setting_override = True) # Setting override will be added later
|
||||||
copy.setTransformation(self.getLocalTransformation())
|
copy.setTransformation(self.getLocalTransformation())
|
||||||
copy.setMeshData(self._mesh_data)
|
copy.setMeshData(self._mesh_data)
|
||||||
copy.setVisible(deepcopy(self._visible, memo))
|
copy.setVisible(deepcopy(self._visible, memo))
|
||||||
|
|
|
@ -334,10 +334,13 @@ class ContainerManager(QObject):
|
||||||
|
|
||||||
# Go through global and extruder stacks and clear their topmost container (the user settings).
|
# Go through global and extruder stacks and clear their topmost container (the user settings).
|
||||||
for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
|
for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
|
||||||
container = stack.getTop()
|
container = stack.userChanges
|
||||||
container.clear()
|
container.clear()
|
||||||
send_emits_containers.append(container)
|
send_emits_containers.append(container)
|
||||||
|
|
||||||
|
# user changes are possibly added to make the current setup match the current enabled extruders
|
||||||
|
Application.getInstance().getMachineManager().correctExtruderSettings()
|
||||||
|
|
||||||
for container in send_emits_containers:
|
for container in send_emits_containers:
|
||||||
container.sendPostponedEmits()
|
container.sendPostponedEmits()
|
||||||
|
|
||||||
|
@ -345,15 +348,18 @@ class ContainerManager(QObject):
|
||||||
#
|
#
|
||||||
# \param material_id \type{str} the id of the material for which to get the linked materials.
|
# \param material_id \type{str} the id of the material for which to get the linked materials.
|
||||||
# \return \type{list} a list of names of materials with the same GUID
|
# \return \type{list} a list of names of materials with the same GUID
|
||||||
@pyqtSlot("QVariant", result = "QStringList")
|
@pyqtSlot("QVariant", bool, result = "QStringList")
|
||||||
def getLinkedMaterials(self, material_node):
|
def getLinkedMaterials(self, material_node, exclude_self = False):
|
||||||
guid = material_node.metadata["GUID"]
|
guid = material_node.metadata["GUID"]
|
||||||
|
|
||||||
|
self_root_material_id = material_node.metadata["base_file"]
|
||||||
material_group_list = self._material_manager.getMaterialGroupListByGUID(guid)
|
material_group_list = self._material_manager.getMaterialGroupListByGUID(guid)
|
||||||
|
|
||||||
linked_material_names = []
|
linked_material_names = []
|
||||||
if material_group_list:
|
if material_group_list:
|
||||||
for material_group in material_group_list:
|
for material_group in material_group_list:
|
||||||
|
if exclude_self and material_group.name == self_root_material_id:
|
||||||
|
continue
|
||||||
linked_material_names.append(material_group.root_material_node.metadata["name"])
|
linked_material_names.append(material_group.root_material_node.metadata["name"])
|
||||||
return linked_material_names
|
return linked_material_names
|
||||||
|
|
||||||
|
@ -389,8 +395,6 @@ class ContainerManager(QObject):
|
||||||
return ContainerManager.getInstance()
|
return ContainerManager.getInstance()
|
||||||
|
|
||||||
def _performMerge(self, merge_into, merge, clear_settings = True):
|
def _performMerge(self, merge_into, merge, clear_settings = True):
|
||||||
assert isinstance(merge, type(merge_into))
|
|
||||||
|
|
||||||
if merge == merge_into:
|
if merge == merge_into:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ from .ExtruderManager import ExtruderManager
|
||||||
|
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
||||||
|
from cura.ProfileReader import NoProfileException
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
@ -172,12 +173,13 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
plugin_registry = PluginRegistry.getInstance()
|
plugin_registry = PluginRegistry.getInstance()
|
||||||
extension = file_name.split(".")[-1]
|
extension = file_name.split(".")[-1]
|
||||||
|
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if not global_container_stack:
|
if not global_stack:
|
||||||
return
|
return
|
||||||
|
|
||||||
machine_extruders = list(ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()))
|
machine_extruders = []
|
||||||
machine_extruders.sort(key = lambda k: k.getMetaDataEntry("position"))
|
for position in sorted(global_stack.extruders):
|
||||||
|
machine_extruders.append(global_stack.extruders[position])
|
||||||
|
|
||||||
for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
|
for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
|
||||||
if meta_data["profile_reader"][0]["extension"] != extension:
|
if meta_data["profile_reader"][0]["extension"] != extension:
|
||||||
|
@ -185,6 +187,8 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
profile_reader = plugin_registry.getPluginObject(plugin_id)
|
profile_reader = plugin_registry.getPluginObject(plugin_id)
|
||||||
try:
|
try:
|
||||||
profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
|
profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
|
||||||
|
except NoProfileException:
|
||||||
|
return { "status": "ok", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "No custom profile to import in file <filename>{0}</filename>", file_name)}
|
||||||
except Exception as e:
|
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.
|
# 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))
|
Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name,profile_reader.getPluginId(), str(e))
|
||||||
|
@ -197,28 +201,51 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
|
|
||||||
# First check if this profile is suitable for this machine
|
# First check if this profile is suitable for this machine
|
||||||
global_profile = None
|
global_profile = None
|
||||||
|
extruder_profiles = []
|
||||||
if len(profile_or_list) == 1:
|
if len(profile_or_list) == 1:
|
||||||
global_profile = profile_or_list[0]
|
global_profile = profile_or_list[0]
|
||||||
else:
|
else:
|
||||||
for profile in profile_or_list:
|
for profile in profile_or_list:
|
||||||
if not profile.getMetaDataEntry("extruder"):
|
if not profile.getMetaDataEntry("position"):
|
||||||
global_profile = profile
|
global_profile = profile
|
||||||
break
|
else:
|
||||||
|
extruder_profiles.append(profile)
|
||||||
|
extruder_profiles = sorted(extruder_profiles, key = lambda x: int(x.getMetaDataEntry("position")))
|
||||||
|
profile_or_list = [global_profile] + extruder_profiles
|
||||||
|
|
||||||
if not global_profile:
|
if not global_profile:
|
||||||
Logger.log("e", "Incorrect profile [%s]. Could not find global profile", file_name)
|
Logger.log("e", "Incorrect profile [%s]. Could not find global profile", file_name)
|
||||||
return { "status": "error",
|
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)}
|
"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")
|
profile_definition = global_profile.getMetaDataEntry("definition")
|
||||||
expected_machine_definition = "fdmprinter"
|
|
||||||
if parseBool(global_container_stack.getMetaDataEntry("has_machine_quality", "False")):
|
# Make sure we have a profile_definition in the file:
|
||||||
expected_machine_definition = global_container_stack.getMetaDataEntry("quality_definition")
|
if profile_definition is None:
|
||||||
if not expected_machine_definition:
|
break
|
||||||
expected_machine_definition = global_container_stack.definition.getId()
|
machine_definition = self.findDefinitionContainers(id = profile_definition)
|
||||||
if expected_machine_definition is not None and profile_definition is not None and profile_definition != expected_machine_definition:
|
if not machine_definition:
|
||||||
|
Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
machine_definition = machine_definition[0]
|
||||||
|
|
||||||
|
# Get the expected machine definition.
|
||||||
|
# i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode...
|
||||||
|
profile_definition = getMachineDefinitionIDForQualitySearch(machine_definition)
|
||||||
|
expected_machine_definition = getMachineDefinitionIDForQualitySearch(global_stack.definition)
|
||||||
|
|
||||||
|
# And check if the profile_definition matches either one (showing error if not):
|
||||||
|
if 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, 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, profile_definition, expected_machine_definition)
|
||||||
return { "status": "error",
|
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> ({1}) doesn't match with your current machine ({2}), could not import it.", file_name, profile_definition, expected_machine_definition)}
|
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "The machine defined in profile <filename>{0}</filename> ({1}) doesn't match with your current machine ({2}), could not import it.", file_name, profile_definition, expected_machine_definition)}
|
||||||
|
|
||||||
|
# Fix the global quality profile's definition field in case it's not correct
|
||||||
|
global_profile.setMetaDataEntry("definition", expected_machine_definition)
|
||||||
|
quality_name = global_profile.getName()
|
||||||
|
quality_type = global_profile.getMetaDataEntry("quality_type")
|
||||||
|
|
||||||
name_seed = os.path.splitext(os.path.basename(file_name))[0]
|
name_seed = os.path.splitext(os.path.basename(file_name))[0]
|
||||||
new_name = self.uniqueName(name_seed)
|
new_name = self.uniqueName(name_seed)
|
||||||
|
|
||||||
|
@ -230,25 +257,25 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
if len(profile_or_list) == 1:
|
if len(profile_or_list) == 1:
|
||||||
global_profile = profile_or_list[0]
|
global_profile = profile_or_list[0]
|
||||||
extruder_profiles = []
|
extruder_profiles = []
|
||||||
for idx, extruder in enumerate(global_container_stack.extruders.values()):
|
for idx, extruder in enumerate(global_stack.extruders.values()):
|
||||||
profile_id = ContainerRegistry.getInstance().uniqueName(global_container_stack.getId() + "_extruder_" + str(idx + 1))
|
profile_id = ContainerRegistry.getInstance().uniqueName(global_stack.getId() + "_extruder_" + str(idx + 1))
|
||||||
profile = InstanceContainer(profile_id)
|
profile = InstanceContainer(profile_id)
|
||||||
profile.setName(global_profile.getName())
|
profile.setName(quality_name)
|
||||||
profile.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
profile.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
||||||
profile.addMetaDataEntry("type", "quality_changes")
|
profile.addMetaDataEntry("type", "quality_changes")
|
||||||
profile.addMetaDataEntry("definition", global_profile.getMetaDataEntry("definition"))
|
profile.addMetaDataEntry("definition", expected_machine_definition)
|
||||||
profile.addMetaDataEntry("quality_type", global_profile.getMetaDataEntry("quality_type"))
|
profile.addMetaDataEntry("quality_type", quality_type)
|
||||||
profile.addMetaDataEntry("extruder", extruder.getId())
|
profile.addMetaDataEntry("position", "0")
|
||||||
profile.setDirty(True)
|
profile.setDirty(True)
|
||||||
if idx == 0:
|
if idx == 0:
|
||||||
# move all per-extruder settings to the first extruder's quality_changes
|
# move all per-extruder settings to the first extruder's quality_changes
|
||||||
for qc_setting_key in global_profile.getAllKeys():
|
for qc_setting_key in global_profile.getAllKeys():
|
||||||
settable_per_extruder = global_container_stack.getProperty(qc_setting_key,
|
settable_per_extruder = global_stack.getProperty(qc_setting_key,
|
||||||
"settable_per_extruder")
|
"settable_per_extruder")
|
||||||
if settable_per_extruder:
|
if settable_per_extruder:
|
||||||
setting_value = global_profile.getProperty(qc_setting_key, "value")
|
setting_value = global_profile.getProperty(qc_setting_key, "value")
|
||||||
|
|
||||||
setting_definition = global_container_stack.getSettingDefinition(qc_setting_key)
|
setting_definition = global_stack.getSettingDefinition(qc_setting_key)
|
||||||
new_instance = SettingInstance(setting_definition, profile)
|
new_instance = SettingInstance(setting_definition, profile)
|
||||||
new_instance.setProperty("value", setting_value)
|
new_instance.setProperty("value", setting_value)
|
||||||
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
||||||
|
@ -265,21 +292,22 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
for profile_index, profile in enumerate(profile_or_list):
|
for profile_index, profile in enumerate(profile_or_list):
|
||||||
if profile_index == 0:
|
if profile_index == 0:
|
||||||
# This is assumed to be the global profile
|
# This is assumed to be the global profile
|
||||||
profile_id = (global_container_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
|
profile_id = (global_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
|
||||||
|
|
||||||
elif profile_index < len(machine_extruders) + 1:
|
elif profile_index < len(machine_extruders) + 1:
|
||||||
# This is assumed to be an extruder profile
|
# This is assumed to be an extruder profile
|
||||||
extruder_id = machine_extruders[profile_index - 1].definition.getId()
|
extruder_id = machine_extruders[profile_index - 1].definition.getId()
|
||||||
if not profile.getMetaDataEntry("extruder"):
|
extruder_position = str(profile_index - 1)
|
||||||
profile.addMetaDataEntry("extruder", extruder_id)
|
if not profile.getMetaDataEntry("position"):
|
||||||
|
profile.addMetaDataEntry("position", extruder_position)
|
||||||
else:
|
else:
|
||||||
profile.setMetaDataEntry("extruder", extruder_id)
|
profile.setMetaDataEntry("position", extruder_position)
|
||||||
profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
|
profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
|
||||||
|
|
||||||
else: #More extruders in the imported file than in the machine.
|
else: #More extruders in the imported file than in the machine.
|
||||||
continue #Delete the additional profiles.
|
continue #Delete the additional profiles.
|
||||||
|
|
||||||
result = self._configureProfile(profile, profile_id, new_name)
|
result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
return {"status": "error", "message": catalog.i18nc(
|
return {"status": "error", "message": catalog.i18nc(
|
||||||
"@info:status Don't translate the XML tags <filename> or <message>!",
|
"@info:status Don't translate the XML tags <filename> or <message>!",
|
||||||
|
@ -307,7 +335,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
# \param new_name The new name for the profile.
|
# \param new_name The new name for the profile.
|
||||||
#
|
#
|
||||||
# \return None if configuring was successful or an error message if an error occurred.
|
# \return None if configuring was successful or an error message if an error occurred.
|
||||||
def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str) -> Optional[str]:
|
def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str, machine_definition_id: str) -> Optional[str]:
|
||||||
profile.setDirty(True) # Ensure the profiles are correctly saved
|
profile.setDirty(True) # Ensure the profiles are correctly saved
|
||||||
|
|
||||||
new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile"))
|
new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile"))
|
||||||
|
@ -317,6 +345,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
# Set the unique Id to the profile, so it's generating a new one even if the user imports the same profile
|
# 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
|
# It also solves an issue with importing profiles from G-Codes
|
||||||
profile.setMetaDataEntry("id", new_id)
|
profile.setMetaDataEntry("id", new_id)
|
||||||
|
profile.setMetaDataEntry("definition", machine_definition_id)
|
||||||
|
|
||||||
if "type" in profile.getMetaData():
|
if "type" in profile.getMetaData():
|
||||||
profile.setMetaDataEntry("type", "quality_changes")
|
profile.setMetaDataEntry("type", "quality_changes")
|
||||||
|
@ -327,9 +356,8 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
if not quality_type:
|
if not quality_type:
|
||||||
return catalog.i18nc("@info:status", "Profile is missing a quality type.")
|
return catalog.i18nc("@info:status", "Profile is missing a quality type.")
|
||||||
|
|
||||||
quality_type_criteria = {"quality_type": quality_type}
|
|
||||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
definition_id = getMachineDefinitionIDForQualitySearch(global_stack)
|
definition_id = getMachineDefinitionIDForQualitySearch(global_stack.definition)
|
||||||
profile.setDefinition(definition_id)
|
profile.setDefinition(definition_id)
|
||||||
|
|
||||||
# Check to make sure the imported profile actually makes sense in context of the current configuration.
|
# Check to make sure the imported profile actually makes sense in context of the current configuration.
|
||||||
|
|
|
@ -8,6 +8,7 @@ from typing import Any, Optional
|
||||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
from UM.Decorators import override
|
from UM.Decorators import override
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Settings.ContainerStack import ContainerStack, InvalidContainerStackError
|
from UM.Settings.ContainerStack import ContainerStack, InvalidContainerStackError
|
||||||
|
@ -314,6 +315,13 @@ class CuraContainerStack(ContainerStack):
|
||||||
|
|
||||||
return cls._findInstanceContainerDefinitionId(definitions[0])
|
return cls._findInstanceContainerDefinitionId(definitions[0])
|
||||||
|
|
||||||
|
## getProperty for extruder positions, with translation from -1 to default extruder number
|
||||||
|
def getExtruderPositionValueWithDefault(self, key):
|
||||||
|
value = self.getProperty(key, "value")
|
||||||
|
if value == -1:
|
||||||
|
value = int(Application.getInstance().getMachineManager().defaultExtruderPosition)
|
||||||
|
return value
|
||||||
|
|
||||||
## private:
|
## private:
|
||||||
|
|
||||||
# Private helper class to keep track of container positions and their types.
|
# Private helper class to keep track of container positions and their types.
|
||||||
|
|
|
@ -241,6 +241,13 @@ class ExtruderManager(QObject):
|
||||||
result.append(extruder_stack.getProperty(setting_key, prop))
|
result.append(extruder_stack.getProperty(setting_key, prop))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def extruderValueWithDefault(self, value):
|
||||||
|
machine_manager = self._application.getMachineManager()
|
||||||
|
if value == "-1":
|
||||||
|
return machine_manager.defaultExtruderPosition
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
## Gets the extruder stacks that are actually being used at the moment.
|
## Gets the extruder stacks that are actually being used at the moment.
|
||||||
#
|
#
|
||||||
# An extruder stack is being used if it is the extruder to print any mesh
|
# An extruder stack is being used if it is the extruder to print any mesh
|
||||||
|
@ -252,7 +259,7 @@ class ExtruderManager(QObject):
|
||||||
#
|
#
|
||||||
# \return A list of extruder stacks.
|
# \return A list of extruder stacks.
|
||||||
def getUsedExtruderStacks(self) -> List["ContainerStack"]:
|
def getUsedExtruderStacks(self) -> List["ContainerStack"]:
|
||||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
global_stack = self._application.getGlobalContainerStack()
|
||||||
container_registry = ContainerRegistry.getInstance()
|
container_registry = ContainerRegistry.getInstance()
|
||||||
|
|
||||||
used_extruder_stack_ids = set()
|
used_extruder_stack_ids = set()
|
||||||
|
@ -302,16 +309,19 @@ class ExtruderManager(QObject):
|
||||||
|
|
||||||
# Check support extruders
|
# Check support extruders
|
||||||
if support_enabled:
|
if support_enabled:
|
||||||
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_infill_extruder_nr", "value"))])
|
used_extruder_stack_ids.add(self.extruderIds[self.extruderValueWithDefault(str(global_stack.getProperty("support_infill_extruder_nr", "value")))])
|
||||||
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_extruder_nr_layer_0", "value"))])
|
used_extruder_stack_ids.add(self.extruderIds[self.extruderValueWithDefault(str(global_stack.getProperty("support_extruder_nr_layer_0", "value")))])
|
||||||
if support_bottom_enabled:
|
if support_bottom_enabled:
|
||||||
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_bottom_extruder_nr", "value"))])
|
used_extruder_stack_ids.add(self.extruderIds[self.extruderValueWithDefault(str(global_stack.getProperty("support_bottom_extruder_nr", "value")))])
|
||||||
if support_roof_enabled:
|
if support_roof_enabled:
|
||||||
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_roof_extruder_nr", "value"))])
|
used_extruder_stack_ids.add(self.extruderIds[self.extruderValueWithDefault(str(global_stack.getProperty("support_roof_extruder_nr", "value")))])
|
||||||
|
|
||||||
# The platform adhesion extruder. Not used if using none.
|
# The platform adhesion extruder. Not used if using none.
|
||||||
if global_stack.getProperty("adhesion_type", "value") != "none":
|
if global_stack.getProperty("adhesion_type", "value") != "none":
|
||||||
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("adhesion_extruder_nr", "value"))])
|
extruder_nr = str(global_stack.getProperty("adhesion_extruder_nr", "value"))
|
||||||
|
if extruder_nr == "-1":
|
||||||
|
extruder_nr = Application.getInstance().getMachineManager().defaultExtruderPosition
|
||||||
|
used_extruder_stack_ids.add(self.extruderIds[extruder_nr])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return [container_registry.findContainerStacks(id = stack_id)[0] for stack_id in used_extruder_stack_ids]
|
return [container_registry.findContainerStacks(id = stack_id)[0] for stack_id in used_extruder_stack_ids]
|
||||||
|
@ -485,6 +495,8 @@ class ExtruderManager(QObject):
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for extruder in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()):
|
for extruder in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()):
|
||||||
|
if not extruder.isEnabled:
|
||||||
|
continue
|
||||||
# only include values from extruders that are "active" for the current machine instance
|
# only include values from extruders that are "active" for the current machine instance
|
||||||
if int(extruder.getMetaDataEntry("position")) >= global_stack.getProperty("machine_extruder_count", "value"):
|
if int(extruder.getMetaDataEntry("position")) >= global_stack.getProperty("machine_extruder_count", "value"):
|
||||||
continue
|
continue
|
||||||
|
@ -656,6 +668,8 @@ class ExtruderManager(QObject):
|
||||||
# global stack if not found.
|
# global stack if not found.
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getExtruderValue(extruder_index, key):
|
def getExtruderValue(extruder_index, key):
|
||||||
|
if extruder_index == -1:
|
||||||
|
extruder_index = int(Application.getInstance().getMachineManager().defaultExtruderPosition)
|
||||||
extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index)
|
extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index)
|
||||||
|
|
||||||
if extruder:
|
if extruder:
|
||||||
|
|
|
@ -3,13 +3,15 @@
|
||||||
|
|
||||||
from typing import Any, TYPE_CHECKING, Optional
|
from typing import Any, TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtProperty
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
from UM.Decorators import override
|
from UM.Decorators import override
|
||||||
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
||||||
from UM.Settings.ContainerStack import ContainerStack
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.Settings.Interfaces import ContainerInterface, PropertyEvaluationContext
|
from UM.Settings.Interfaces import ContainerInterface, PropertyEvaluationContext
|
||||||
|
from UM.Util import parseBool
|
||||||
|
|
||||||
from . import Exceptions
|
from . import Exceptions
|
||||||
from .CuraContainerStack import CuraContainerStack, _ContainerIndexes
|
from .CuraContainerStack import CuraContainerStack, _ContainerIndexes
|
||||||
|
@ -30,6 +32,8 @@ class ExtruderStack(CuraContainerStack):
|
||||||
|
|
||||||
self.propertiesChanged.connect(self._onPropertiesChanged)
|
self.propertiesChanged.connect(self._onPropertiesChanged)
|
||||||
|
|
||||||
|
enabledChanged = pyqtSignal()
|
||||||
|
|
||||||
## Overridden from ContainerStack
|
## Overridden from ContainerStack
|
||||||
#
|
#
|
||||||
# This will set the next stack and ensure that we register this stack as an extruder.
|
# This will set the next stack and ensure that we register this stack as an extruder.
|
||||||
|
@ -46,6 +50,16 @@ class ExtruderStack(CuraContainerStack):
|
||||||
def getNextStack(self) -> Optional["GlobalStack"]:
|
def getNextStack(self) -> Optional["GlobalStack"]:
|
||||||
return super().getNextStack()
|
return super().getNextStack()
|
||||||
|
|
||||||
|
def setEnabled(self, enabled):
|
||||||
|
if "enabled" not in self._metadata:
|
||||||
|
self.addMetaDataEntry("enabled", "True")
|
||||||
|
self.setMetaDataEntry("enabled", str(enabled))
|
||||||
|
self.enabledChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = enabledChanged)
|
||||||
|
def isEnabled(self):
|
||||||
|
return parseBool(self.getMetaDataEntry("enabled", "True"))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def getLoadingPriority(cls) -> int:
|
def getLoadingPriority(cls) -> int:
|
||||||
return 3
|
return 3
|
||||||
|
@ -98,6 +112,8 @@ class ExtruderStack(CuraContainerStack):
|
||||||
|
|
||||||
limit_to_extruder = super().getProperty(key, "limit_to_extruder", context)
|
limit_to_extruder = super().getProperty(key, "limit_to_extruder", context)
|
||||||
if limit_to_extruder is not None:
|
if limit_to_extruder is not None:
|
||||||
|
if limit_to_extruder == -1:
|
||||||
|
limit_to_extruder = int(Application.getInstance().getMachineManager().defaultExtruderPosition)
|
||||||
limit_to_extruder = str(limit_to_extruder)
|
limit_to_extruder = str(limit_to_extruder)
|
||||||
if (limit_to_extruder is not None and limit_to_extruder != "-1") and self.getMetaDataEntry("position") != str(limit_to_extruder):
|
if (limit_to_extruder is not None and limit_to_extruder != "-1") and self.getMetaDataEntry("position") != str(limit_to_extruder):
|
||||||
if str(limit_to_extruder) in self.getNextStack().extruders:
|
if str(limit_to_extruder) in self.getNextStack().extruders:
|
||||||
|
@ -120,6 +136,8 @@ class ExtruderStack(CuraContainerStack):
|
||||||
@override(CuraContainerStack)
|
@override(CuraContainerStack)
|
||||||
def deserialize(self, contents: str, file_name: Optional[str] = None) -> None:
|
def deserialize(self, contents: str, file_name: Optional[str] = None) -> None:
|
||||||
super().deserialize(contents, file_name)
|
super().deserialize(contents, file_name)
|
||||||
|
if "enabled" not in self.getMetaData():
|
||||||
|
self.addMetaDataEntry("enabled", "True")
|
||||||
stacks = ContainerRegistry.getInstance().findContainerStacks(id=self.getMetaDataEntry("machine", ""))
|
stacks = ContainerRegistry.getInstance().findContainerStacks(id=self.getMetaDataEntry("machine", ""))
|
||||||
if stacks:
|
if stacks:
|
||||||
self.setNextStack(stacks[0])
|
self.setNextStack(stacks[0])
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty, QTimer
|
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot, pyqtProperty, QTimer
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
@ -24,6 +24,8 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
|
|
||||||
## Human-readable name of the extruder.
|
## Human-readable name of the extruder.
|
||||||
NameRole = Qt.UserRole + 2
|
NameRole = Qt.UserRole + 2
|
||||||
|
## Is the extruder enabled?
|
||||||
|
EnabledRole = Qt.UserRole + 9
|
||||||
|
|
||||||
## Colour of the material loaded in the extruder.
|
## Colour of the material loaded in the extruder.
|
||||||
ColorRole = Qt.UserRole + 3
|
ColorRole = Qt.UserRole + 3
|
||||||
|
@ -43,6 +45,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
|
|
||||||
# The variant of the extruder.
|
# The variant of the extruder.
|
||||||
VariantRole = Qt.UserRole + 7
|
VariantRole = Qt.UserRole + 7
|
||||||
|
StackRole = Qt.UserRole + 8
|
||||||
|
|
||||||
## List of colours to display if there is no material or the material has no known
|
## List of colours to display if there is no material or the material has no known
|
||||||
# colour.
|
# colour.
|
||||||
|
@ -57,11 +60,13 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
|
|
||||||
self.addRoleName(self.IdRole, "id")
|
self.addRoleName(self.IdRole, "id")
|
||||||
self.addRoleName(self.NameRole, "name")
|
self.addRoleName(self.NameRole, "name")
|
||||||
|
self.addRoleName(self.EnabledRole, "enabled")
|
||||||
self.addRoleName(self.ColorRole, "color")
|
self.addRoleName(self.ColorRole, "color")
|
||||||
self.addRoleName(self.IndexRole, "index")
|
self.addRoleName(self.IndexRole, "index")
|
||||||
self.addRoleName(self.DefinitionRole, "definition")
|
self.addRoleName(self.DefinitionRole, "definition")
|
||||||
self.addRoleName(self.MaterialRole, "material")
|
self.addRoleName(self.MaterialRole, "material")
|
||||||
self.addRoleName(self.VariantRole, "variant")
|
self.addRoleName(self.VariantRole, "variant")
|
||||||
|
self.addRoleName(self.StackRole, "stack")
|
||||||
|
|
||||||
self._update_extruder_timer = QTimer()
|
self._update_extruder_timer = QTimer()
|
||||||
self._update_extruder_timer.setInterval(100)
|
self._update_extruder_timer.setInterval(100)
|
||||||
|
@ -183,11 +188,13 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
item = {
|
item = {
|
||||||
"id": extruder.getId(),
|
"id": extruder.getId(),
|
||||||
"name": extruder.getName(),
|
"name": extruder.getName(),
|
||||||
|
"enabled": extruder.isEnabled,
|
||||||
"color": color,
|
"color": color,
|
||||||
"index": position,
|
"index": position,
|
||||||
"definition": extruder.getBottom().getId(),
|
"definition": extruder.getBottom().getId(),
|
||||||
"material": extruder.material.getName() if extruder.material else "",
|
"material": extruder.material.getName() if extruder.material else "",
|
||||||
"variant": extruder.variant.getName() if extruder.variant else "", # e.g. print core
|
"variant": extruder.variant.getName() if extruder.variant else "", # e.g. print core
|
||||||
|
"stack": extruder,
|
||||||
}
|
}
|
||||||
|
|
||||||
items.append(item)
|
items.append(item)
|
||||||
|
@ -203,6 +210,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
item = {
|
item = {
|
||||||
"id": "",
|
"id": "",
|
||||||
"name": catalog.i18nc("@menuitem", "Not overridden"),
|
"name": catalog.i18nc("@menuitem", "Not overridden"),
|
||||||
|
"enabled": True,
|
||||||
"color": "#ffffff",
|
"color": "#ffffff",
|
||||||
"index": -1,
|
"index": -1,
|
||||||
"definition": ""
|
"definition": ""
|
||||||
|
|
|
@ -7,6 +7,7 @@ from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtProperty
|
from PyQt5.QtCore import pyqtProperty
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
from UM.Decorators import override
|
from UM.Decorators import override
|
||||||
|
|
||||||
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
||||||
|
@ -104,6 +105,8 @@ class GlobalStack(CuraContainerStack):
|
||||||
# Handle the "limit_to_extruder" property.
|
# Handle the "limit_to_extruder" property.
|
||||||
limit_to_extruder = super().getProperty(key, "limit_to_extruder", context)
|
limit_to_extruder = super().getProperty(key, "limit_to_extruder", context)
|
||||||
if limit_to_extruder is not None:
|
if limit_to_extruder is not None:
|
||||||
|
if limit_to_extruder == -1:
|
||||||
|
limit_to_extruder = int(Application.getInstance().getMachineManager().defaultExtruderPosition)
|
||||||
limit_to_extruder = str(limit_to_extruder)
|
limit_to_extruder = str(limit_to_extruder)
|
||||||
if limit_to_extruder is not None and limit_to_extruder != "-1" and limit_to_extruder in self._extruders:
|
if limit_to_extruder is not None and limit_to_extruder != "-1" and limit_to_extruder in self._extruders:
|
||||||
if super().getProperty(key, "settable_per_extruder", context):
|
if super().getProperty(key, "settable_per_extruder", context):
|
||||||
|
|
|
@ -4,13 +4,12 @@
|
||||||
import collections
|
import collections
|
||||||
import time
|
import time
|
||||||
#Type hinting.
|
#Type hinting.
|
||||||
from typing import Union, List, Dict, TYPE_CHECKING, Optional
|
from typing import List, Dict, TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
from UM.Signal import Signal
|
from UM.Signal import Signal
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, QTimer
|
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, QTimer
|
||||||
import UM.FlameProfiler
|
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
from UM import Util
|
from UM import Util
|
||||||
|
|
||||||
|
@ -20,13 +19,14 @@ from UM.Logger import Logger
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
|
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
|
||||||
from UM.Settings.SettingFunction import SettingFunction
|
from UM.Settings.SettingFunction import SettingFunction
|
||||||
from UM.Signal import postponeSignals, CompressTechnique
|
from UM.Signal import postponeSignals, CompressTechnique
|
||||||
|
|
||||||
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
||||||
|
|
||||||
from cura.PrinterOutputDevice import PrinterOutputDevice
|
from cura.PrinterOutputDevice import PrinterOutputDevice
|
||||||
|
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
||||||
|
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||||
|
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
|
||||||
from .CuraStackBuilder import CuraStackBuilder
|
from .CuraStackBuilder import CuraStackBuilder
|
||||||
|
@ -48,16 +48,12 @@ class MachineManager(QObject):
|
||||||
self._global_container_stack = None # type: GlobalStack
|
self._global_container_stack = None # type: GlobalStack
|
||||||
|
|
||||||
self._current_root_material_id = {}
|
self._current_root_material_id = {}
|
||||||
self._current_root_material_name = {}
|
|
||||||
self._current_quality_group = None
|
self._current_quality_group = None
|
||||||
self._current_quality_changes_group = None
|
self._current_quality_changes_group = None
|
||||||
|
|
||||||
self.machine_extruder_material_update_dict = collections.defaultdict(list)
|
self._default_extruder_position = "0" # to be updated when extruders are switched on and off
|
||||||
|
|
||||||
self._error_check_timer = QTimer()
|
self.machine_extruder_material_update_dict = collections.defaultdict(list)
|
||||||
self._error_check_timer.setInterval(250)
|
|
||||||
self._error_check_timer.setSingleShot(True)
|
|
||||||
self._error_check_timer.timeout.connect(self._updateStacksHaveErrors)
|
|
||||||
|
|
||||||
self._instance_container_timer = QTimer()
|
self._instance_container_timer = QTimer()
|
||||||
self._instance_container_timer.setInterval(250)
|
self._instance_container_timer.setInterval(250)
|
||||||
|
@ -107,6 +103,12 @@ class MachineManager(QObject):
|
||||||
# There might already be some output devices by the time the signal is connected
|
# There might already be some output devices by the time the signal is connected
|
||||||
self._onOutputDevicesChanged()
|
self._onOutputDevicesChanged()
|
||||||
|
|
||||||
|
self._current_printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer
|
||||||
|
self.activeMaterialChanged.connect(self._onCurrentConfigurationChanged)
|
||||||
|
self.activeVariantChanged.connect(self._onCurrentConfigurationChanged)
|
||||||
|
# Force to compute the current configuration
|
||||||
|
self._onCurrentConfigurationChanged()
|
||||||
|
|
||||||
self._application.callLater(self.setInitialActiveMachine)
|
self._application.callLater(self.setInitialActiveMachine)
|
||||||
|
|
||||||
self._material_incompatible_message = Message(catalog.i18nc("@info:status",
|
self._material_incompatible_message = Message(catalog.i18nc("@info:status",
|
||||||
|
@ -117,12 +119,17 @@ class MachineManager(QObject):
|
||||||
if containers:
|
if containers:
|
||||||
containers[0].nameChanged.connect(self._onMaterialNameChanged)
|
containers[0].nameChanged.connect(self._onMaterialNameChanged)
|
||||||
|
|
||||||
self._material_manager = self._application._material_manager
|
self._material_manager = self._application.getMaterialManager()
|
||||||
|
self._variant_manager = self._application.getVariantManager()
|
||||||
self._quality_manager = self._application.getQualityManager()
|
self._quality_manager = self._application.getQualityManager()
|
||||||
|
|
||||||
# When the materials lookup table gets updated, it can mean that a material has its name changed, which should
|
# When the materials lookup table gets updated, it can mean that a material has its name changed, which should
|
||||||
# be reflected on the GUI. This signal emission makes sure that it happens.
|
# be reflected on the GUI. This signal emission makes sure that it happens.
|
||||||
self._material_manager.materialsUpdated.connect(self.rootMaterialChanged)
|
self._material_manager.materialsUpdated.connect(self.rootMaterialChanged)
|
||||||
|
# When the materials get updated, it can be that an activated material's diameter gets changed. In that case,
|
||||||
|
# a material update should be triggered to make sure that the machine still has compatible materials activated.
|
||||||
|
self._material_manager.materialsUpdated.connect(self._updateUponMaterialMetadataChange)
|
||||||
|
self.rootMaterialChanged.connect(self._onRootMaterialChanged)
|
||||||
|
|
||||||
activeQualityGroupChanged = pyqtSignal()
|
activeQualityGroupChanged = pyqtSignal()
|
||||||
activeQualityChangesGroupChanged = pyqtSignal()
|
activeQualityChangesGroupChanged = pyqtSignal()
|
||||||
|
@ -132,15 +139,18 @@ class MachineManager(QObject):
|
||||||
activeVariantChanged = pyqtSignal()
|
activeVariantChanged = pyqtSignal()
|
||||||
activeQualityChanged = pyqtSignal()
|
activeQualityChanged = pyqtSignal()
|
||||||
activeStackChanged = pyqtSignal() # Emitted whenever the active stack is changed (ie: when changing between extruders, changing a profile, but not when changing a value)
|
activeStackChanged = pyqtSignal() # Emitted whenever the active stack is changed (ie: when changing between extruders, changing a profile, but not when changing a value)
|
||||||
|
extruderChanged = pyqtSignal()
|
||||||
|
|
||||||
globalValueChanged = pyqtSignal() # Emitted whenever a value inside global container is changed.
|
globalValueChanged = pyqtSignal() # Emitted whenever a value inside global container is changed.
|
||||||
activeStackValueChanged = pyqtSignal() # Emitted whenever a value inside the active stack is changed.
|
activeStackValueChanged = pyqtSignal() # Emitted whenever a value inside the active stack is changed.
|
||||||
activeStackValidationChanged = pyqtSignal() # Emitted whenever a validation inside active container is changed
|
activeStackValidationChanged = pyqtSignal() # Emitted whenever a validation inside active container is changed
|
||||||
stacksValidationChanged = pyqtSignal() # Emitted whenever a validation is changed
|
stacksValidationChanged = pyqtSignal() # Emitted whenever a validation is changed
|
||||||
|
numberExtrudersEnabledChanged = pyqtSignal() # Emitted when the number of extruders that are enabled changed
|
||||||
|
|
||||||
blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly
|
blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly
|
||||||
|
|
||||||
outputDevicesChanged = pyqtSignal()
|
outputDevicesChanged = pyqtSignal()
|
||||||
|
currentConfigurationChanged = pyqtSignal() # Emitted every time the current configurations of the machine changes
|
||||||
|
|
||||||
rootMaterialChanged = pyqtSignal()
|
rootMaterialChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@ -160,6 +170,39 @@ class MachineManager(QObject):
|
||||||
|
|
||||||
self.outputDevicesChanged.emit()
|
self.outputDevicesChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(QObject, notify = currentConfigurationChanged)
|
||||||
|
def currentConfiguration(self):
|
||||||
|
return self._current_printer_configuration
|
||||||
|
|
||||||
|
def _onCurrentConfigurationChanged(self) -> None:
|
||||||
|
if not self._global_container_stack:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create the configuration model with the current data in Cura
|
||||||
|
self._current_printer_configuration.printerType = self._global_container_stack.definition.getName()
|
||||||
|
self._current_printer_configuration.extruderConfigurations = []
|
||||||
|
for extruder in self._global_container_stack.extruders.values():
|
||||||
|
extruder_configuration = ExtruderConfigurationModel()
|
||||||
|
# For compare just the GUID is needed at this moment
|
||||||
|
mat_type = extruder.material.getMetaDataEntry("material") if extruder.material != self._empty_material_container else None
|
||||||
|
mat_guid = extruder.material.getMetaDataEntry("GUID") if extruder.material != self._empty_material_container else None
|
||||||
|
mat_color = extruder.material.getMetaDataEntry("color_name") if extruder.material != self._empty_material_container else None
|
||||||
|
mat_brand = extruder.material.getMetaDataEntry("brand") if extruder.material != self._empty_material_container else None
|
||||||
|
mat_name = extruder.material.getMetaDataEntry("name") if extruder.material != self._empty_material_container else None
|
||||||
|
material_model = MaterialOutputModel(mat_guid, mat_type, mat_color, mat_brand, mat_name)
|
||||||
|
|
||||||
|
extruder_configuration.position = int(extruder.getMetaDataEntry("position"))
|
||||||
|
extruder_configuration.material = material_model
|
||||||
|
extruder_configuration.hotendID = extruder.variant.getName() if extruder.variant != self._empty_variant_container else None
|
||||||
|
self._current_printer_configuration.extruderConfigurations.append(extruder_configuration)
|
||||||
|
|
||||||
|
self._current_printer_configuration.buildplateConfiguration = self._global_container_stack.getProperty("machine_buildplate_type", "value") if self._global_container_stack.variant != self._empty_variant_container else None
|
||||||
|
self.currentConfigurationChanged.emit()
|
||||||
|
|
||||||
|
@pyqtSlot(QObject, result = bool)
|
||||||
|
def matchesConfiguration(self, configuration: ConfigurationModel) -> bool:
|
||||||
|
return self._current_printer_configuration == configuration
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify = outputDevicesChanged)
|
@pyqtProperty("QVariantList", notify = outputDevicesChanged)
|
||||||
def printerOutputDevices(self):
|
def printerOutputDevices(self):
|
||||||
return self._printer_output_devices
|
return self._printer_output_devices
|
||||||
|
@ -189,7 +232,9 @@ class MachineManager(QObject):
|
||||||
|
|
||||||
# Update the local global container stack reference
|
# Update the local global container stack reference
|
||||||
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
|
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if self._global_container_stack:
|
||||||
|
self.updateDefaultExtruder()
|
||||||
|
self.updateNumberExtrudersEnabled()
|
||||||
self.globalContainerChanged.emit()
|
self.globalContainerChanged.emit()
|
||||||
|
|
||||||
# after switching the global stack we reconnect all the signals and set the variant and material references
|
# after switching the global stack we reconnect all the signals and set the variant and material references
|
||||||
|
@ -222,15 +267,6 @@ class MachineManager(QObject):
|
||||||
del self.machine_extruder_material_update_dict[self._global_container_stack.getId()]
|
del self.machine_extruder_material_update_dict[self._global_container_stack.getId()]
|
||||||
|
|
||||||
self.activeQualityGroupChanged.emit()
|
self.activeQualityGroupChanged.emit()
|
||||||
self._error_check_timer.start()
|
|
||||||
|
|
||||||
## Update self._stacks_valid according to _checkStacksForErrors and emit if change.
|
|
||||||
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) -> None:
|
def _onActiveExtruderStackChanged(self) -> None:
|
||||||
self.blurSettings.emit() # Ensure no-one has focus.
|
self.blurSettings.emit() # Ensure no-one has focus.
|
||||||
|
@ -250,8 +286,6 @@ class MachineManager(QObject):
|
||||||
|
|
||||||
self.rootMaterialChanged.emit()
|
self.rootMaterialChanged.emit()
|
||||||
|
|
||||||
self._error_check_timer.start()
|
|
||||||
|
|
||||||
def _onInstanceContainersChanged(self, container) -> None:
|
def _onInstanceContainersChanged(self, container) -> None:
|
||||||
self._instance_container_timer.start()
|
self._instance_container_timer.start()
|
||||||
|
|
||||||
|
@ -260,9 +294,6 @@ class MachineManager(QObject):
|
||||||
# Notify UI items, such as the "changed" star in profile pull down menu.
|
# Notify UI items, such as the "changed" star in profile pull down menu.
|
||||||
self.activeStackValueChanged.emit()
|
self.activeStackValueChanged.emit()
|
||||||
|
|
||||||
elif property_name == "validationState":
|
|
||||||
self._error_check_timer.start()
|
|
||||||
|
|
||||||
## Given a global_stack, make sure that it's all valid by searching for this quality group and applying it again
|
## Given a global_stack, make sure that it's all valid by searching for this quality group and applying it again
|
||||||
def _initMachineState(self, global_stack):
|
def _initMachineState(self, global_stack):
|
||||||
material_dict = {}
|
material_dict = {}
|
||||||
|
@ -306,6 +337,18 @@ class MachineManager(QObject):
|
||||||
|
|
||||||
self.__emitChangedSignals()
|
self.__emitChangedSignals()
|
||||||
|
|
||||||
|
## Given a definition id, return the machine with this id.
|
||||||
|
# Optional: add a list of keys and values to filter the list of machines with the given definition id
|
||||||
|
# \param definition_id \type{str} definition id that needs to look for
|
||||||
|
# \param metadata_filter \type{dict} list of metadata keys and values used for filtering
|
||||||
|
@staticmethod
|
||||||
|
def getMachine(definition_id: str, metadata_filter: Dict[str, str] = None) -> Optional["GlobalStack"]:
|
||||||
|
machines = ContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
|
||||||
|
for machine in machines:
|
||||||
|
if machine.definition.getId() == definition_id:
|
||||||
|
return machine
|
||||||
|
return None
|
||||||
|
|
||||||
@pyqtSlot(str, str)
|
@pyqtSlot(str, str)
|
||||||
def addMachine(self, name: str, definition_id: str) -> None:
|
def addMachine(self, name: str, definition_id: str) -> None:
|
||||||
new_stack = CuraStackBuilder.createMachine(name, definition_id)
|
new_stack = CuraStackBuilder.createMachine(name, definition_id)
|
||||||
|
@ -321,7 +364,7 @@ class MachineManager(QObject):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self._global_container_stack.hasErrors():
|
if self._global_container_stack.hasErrors():
|
||||||
Logger.log("d", "Checking global stack for errors took %0.2f s and we found and error" % (time.time() - time_start))
|
Logger.log("d", "Checking global stack for errors took %0.2f s and we found an error" % (time.time() - time_start))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Not a very pretty solution, but the extruder manager doesn't really know how many extruders there are
|
# Not a very pretty solution, but the extruder manager doesn't really know how many extruders there are
|
||||||
|
@ -405,6 +448,12 @@ class MachineManager(QObject):
|
||||||
def stacksHaveErrors(self) -> bool:
|
def stacksHaveErrors(self) -> bool:
|
||||||
return bool(self._stacks_have_errors)
|
return bool(self._stacks_have_errors)
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = globalContainerChanged)
|
||||||
|
def activeMachineDefinitionName(self) -> str:
|
||||||
|
if self._global_container_stack:
|
||||||
|
return self._global_container_stack.definition.getName()
|
||||||
|
return ""
|
||||||
|
|
||||||
@pyqtProperty(str, notify = globalContainerChanged)
|
@pyqtProperty(str, notify = globalContainerChanged)
|
||||||
def activeMachineName(self) -> str:
|
def activeMachineName(self) -> str:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
|
@ -417,6 +466,18 @@ class MachineManager(QObject):
|
||||||
return self._global_container_stack.getId()
|
return self._global_container_stack.getId()
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = outputDevicesChanged)
|
||||||
|
def activeMachineNetworkKey(self) -> str:
|
||||||
|
if self._global_container_stack:
|
||||||
|
return self._global_container_stack.getMetaDataEntry("um_network_key", "")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = outputDevicesChanged)
|
||||||
|
def activeMachineNetworkGroupName(self) -> str:
|
||||||
|
if self._global_container_stack:
|
||||||
|
return self._global_container_stack.getMetaDataEntry("connect_group_name", "")
|
||||||
|
return ""
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify = globalContainerChanged)
|
@pyqtProperty(QObject, notify = globalContainerChanged)
|
||||||
def activeMachine(self) -> Optional["GlobalStack"]:
|
def activeMachine(self) -> Optional["GlobalStack"]:
|
||||||
return self._global_container_stack
|
return self._global_container_stack
|
||||||
|
@ -566,7 +627,7 @@ class MachineManager(QObject):
|
||||||
@pyqtProperty(str, notify = globalContainerChanged)
|
@pyqtProperty(str, notify = globalContainerChanged)
|
||||||
def activeQualityDefinitionId(self) -> str:
|
def activeQualityDefinitionId(self) -> str:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return getMachineDefinitionIDForQualitySearch(self._global_container_stack)
|
return getMachineDefinitionIDForQualitySearch(self._global_container_stack.definition)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
## Gets how the active definition calls variants
|
## Gets how the active definition calls variants
|
||||||
|
@ -600,12 +661,22 @@ class MachineManager(QObject):
|
||||||
if other_machine_stacks:
|
if other_machine_stacks:
|
||||||
self.setActiveMachine(other_machine_stacks[0]["id"])
|
self.setActiveMachine(other_machine_stacks[0]["id"])
|
||||||
|
|
||||||
|
metadata = ContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)[0]
|
||||||
|
network_key = metadata["um_network_key"] if "um_network_key" in metadata else None
|
||||||
ExtruderManager.getInstance().removeMachineExtruders(machine_id)
|
ExtruderManager.getInstance().removeMachineExtruders(machine_id)
|
||||||
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id)
|
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id)
|
||||||
for container in containers:
|
for container in containers:
|
||||||
ContainerRegistry.getInstance().removeContainer(container["id"])
|
ContainerRegistry.getInstance().removeContainer(container["id"])
|
||||||
ContainerRegistry.getInstance().removeContainer(machine_id)
|
ContainerRegistry.getInstance().removeContainer(machine_id)
|
||||||
|
|
||||||
|
# If the printer that is being removed is a network printer, the hidden printers have to be also removed
|
||||||
|
if network_key:
|
||||||
|
metadata_filter = {"um_network_key": network_key}
|
||||||
|
hidden_containers = ContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
|
||||||
|
if hidden_containers:
|
||||||
|
# This reuses the method and remove all printers recursively
|
||||||
|
self.removeMachine(hidden_containers[0].getId())
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||||
def hasMaterials(self) -> bool:
|
def hasMaterials(self) -> bool:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
|
@ -633,6 +704,8 @@ class MachineManager(QObject):
|
||||||
buildplate_compatible = True # It is compatible by default
|
buildplate_compatible = True # It is compatible by default
|
||||||
extruder_stacks = self._global_container_stack.extruders.values()
|
extruder_stacks = self._global_container_stack.extruders.values()
|
||||||
for stack in extruder_stacks:
|
for stack in extruder_stacks:
|
||||||
|
if not stack.isEnabled:
|
||||||
|
continue
|
||||||
material_container = stack.material
|
material_container = stack.material
|
||||||
if material_container == self._empty_material_container:
|
if material_container == self._empty_material_container:
|
||||||
continue
|
continue
|
||||||
|
@ -665,22 +738,6 @@ class MachineManager(QObject):
|
||||||
|
|
||||||
return result
|
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)
|
|
||||||
def filterMaterialsByMachine(self) -> bool:
|
|
||||||
if self._global_container_stack:
|
|
||||||
return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_machine_materials", False))
|
|
||||||
return False
|
|
||||||
|
|
||||||
## Property to indicate if a machine has "specialized" quality profiles.
|
|
||||||
# Some machines have their own quality profiles that "override" the default catch all profiles.
|
|
||||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
|
||||||
def filterQualityByMachine(self) -> bool:
|
|
||||||
if self._global_container_stack:
|
|
||||||
return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_machine_quality", False))
|
|
||||||
return False
|
|
||||||
|
|
||||||
## Get the Definition ID of a machine (specified by ID)
|
## Get the Definition ID of a machine (specified by ID)
|
||||||
# \param machine_id string machine id to get the definition ID of
|
# \param machine_id string machine id to get the definition ID of
|
||||||
# \returns DefinitionID (string) if found, None otherwise
|
# \returns DefinitionID (string) if found, None otherwise
|
||||||
|
@ -690,6 +747,43 @@ class MachineManager(QObject):
|
||||||
if containers:
|
if containers:
|
||||||
return containers[0].definition.getId()
|
return containers[0].definition.getId()
|
||||||
|
|
||||||
|
def getIncompatibleSettingsOnEnabledExtruders(self, container):
|
||||||
|
extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
|
||||||
|
result = []
|
||||||
|
for setting_instance in container.findInstances():
|
||||||
|
setting_key = setting_instance.definition.key
|
||||||
|
setting_enabled = self._global_container_stack.getProperty(setting_key, "enabled")
|
||||||
|
if not setting_enabled:
|
||||||
|
# A setting is not visible anymore
|
||||||
|
result.append(setting_key)
|
||||||
|
Logger.log("d", "Reset setting [%s] from [%s] because the setting is no longer enabled", setting_key, container)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not self._global_container_stack.getProperty(setting_key, "type") in ("extruder", "optional_extruder"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
old_value = container.getProperty(setting_key, "value")
|
||||||
|
if int(old_value) >= extruder_count or not self._global_container_stack.extruders[str(old_value)].isEnabled:
|
||||||
|
result.append(setting_key)
|
||||||
|
Logger.log("d", "Reset setting [%s] in [%s] because its old value [%s] is no longer valid", setting_key, container, old_value)
|
||||||
|
return result
|
||||||
|
|
||||||
|
## Update extruder number to a valid value when the number of extruders are changed, or when an extruder is changed
|
||||||
|
def correctExtruderSettings(self):
|
||||||
|
for setting_key in self.getIncompatibleSettingsOnEnabledExtruders(self._global_container_stack.userChanges):
|
||||||
|
self._global_container_stack.userChanges.removeInstance(setting_key)
|
||||||
|
add_user_changes = self.getIncompatibleSettingsOnEnabledExtruders(self._global_container_stack.qualityChanges)
|
||||||
|
for setting_key in add_user_changes:
|
||||||
|
# Apply quality changes that are incompatible to user changes, so we do not change the quality changes itself.
|
||||||
|
self._global_container_stack.userChanges.setProperty(setting_key, "value", self._default_extruder_position)
|
||||||
|
if add_user_changes:
|
||||||
|
caution_message = Message(catalog.i18nc(
|
||||||
|
"@info:generic",
|
||||||
|
"Settings have been changed to match the current availability of extruders: [%s]" % ", ".join(add_user_changes)),
|
||||||
|
lifetime=0,
|
||||||
|
title = catalog.i18nc("@info:title", "Settings updated"))
|
||||||
|
caution_message.show()
|
||||||
|
|
||||||
## Set the amount of extruders on the active machine (global stack)
|
## Set the amount of extruders on the active machine (global stack)
|
||||||
# \param extruder_count int the number of extruders to set
|
# \param extruder_count int the number of extruders to set
|
||||||
def setActiveMachineExtruderCount(self, extruder_count):
|
def setActiveMachineExtruderCount(self, extruder_count):
|
||||||
|
@ -703,16 +797,11 @@ class MachineManager(QObject):
|
||||||
if extruder_count == previous_extruder_count:
|
if extruder_count == previous_extruder_count:
|
||||||
return
|
return
|
||||||
|
|
||||||
# reset all extruder number settings whose value is no longer valid
|
definition_changes_container.setProperty("machine_extruder_count", "value", extruder_count)
|
||||||
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"))
|
self.updateDefaultExtruder()
|
||||||
if old_value >= extruder_count:
|
self.updateNumberExtrudersEnabled()
|
||||||
self._global_container_stack.userChanges.removeInstance(setting_key)
|
self.correctExtruderSettings()
|
||||||
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
|
# 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()
|
root_node = Application.getInstance().getController().getScene().getRoot()
|
||||||
|
@ -723,21 +812,19 @@ class MachineManager(QObject):
|
||||||
if extruder_nr is not None and int(extruder_nr) > extruder_count - 1:
|
if extruder_nr is not None and int(extruder_nr) > extruder_count - 1:
|
||||||
node.callDecoration("setActiveExtruder", extruder_manager.getExtruderStack(extruder_count - 1).getId())
|
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
|
# Make sure one of the extruder stacks is active
|
||||||
extruder_manager.setActiveExtruderIndex(0)
|
extruder_manager.setActiveExtruderIndex(0)
|
||||||
|
|
||||||
# Move settable_per_extruder values out of the global container
|
# 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.
|
# 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()
|
global_user_container = self._global_container_stack.userChanges
|
||||||
|
|
||||||
# Make sure extruder_stacks exists
|
# Make sure extruder_stacks exists
|
||||||
extruder_stacks = []
|
extruder_stacks = []
|
||||||
|
|
||||||
if previous_extruder_count == 1:
|
if previous_extruder_count == 1:
|
||||||
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||||
global_user_container = self._global_container_stack.getTop()
|
global_user_container = self._global_container_stack.userChanges
|
||||||
|
|
||||||
for setting_instance in global_user_container.findInstances():
|
for setting_instance in global_user_container.findInstances():
|
||||||
setting_key = setting_instance.definition.key
|
setting_key = setting_instance.definition.key
|
||||||
|
@ -746,11 +833,12 @@ class MachineManager(QObject):
|
||||||
if settable_per_extruder:
|
if settable_per_extruder:
|
||||||
limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_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 = extruder_stacks[max(0, limit_to_extruder)]
|
||||||
extruder_stack.getTop().setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value"))
|
extruder_stack.userChanges.setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value"))
|
||||||
global_user_container.removeInstance(setting_key)
|
global_user_container.removeInstance(setting_key)
|
||||||
|
|
||||||
# Signal that the global stack has changed
|
# Signal that the global stack has changed
|
||||||
Application.getInstance().globalContainerStackChanged.emit()
|
Application.getInstance().globalContainerStackChanged.emit()
|
||||||
|
self.forceUpdateAllSettings()
|
||||||
|
|
||||||
@pyqtSlot(int, result = QObject)
|
@pyqtSlot(int, result = QObject)
|
||||||
def getExtruder(self, position: int):
|
def getExtruder(self, position: int):
|
||||||
|
@ -759,6 +847,63 @@ class MachineManager(QObject):
|
||||||
extruder = self._global_container_stack.extruders.get(str(position))
|
extruder = self._global_container_stack.extruders.get(str(position))
|
||||||
return extruder
|
return extruder
|
||||||
|
|
||||||
|
def updateDefaultExtruder(self):
|
||||||
|
extruder_items = sorted(self._global_container_stack.extruders.items())
|
||||||
|
old_position = self._default_extruder_position
|
||||||
|
new_default_position = "0"
|
||||||
|
for position, extruder in extruder_items:
|
||||||
|
if extruder.isEnabled:
|
||||||
|
new_default_position = position
|
||||||
|
break
|
||||||
|
if new_default_position != old_position:
|
||||||
|
self._default_extruder_position = new_default_position
|
||||||
|
self.extruderChanged.emit()
|
||||||
|
|
||||||
|
def updateNumberExtrudersEnabled(self):
|
||||||
|
definition_changes_container = self._global_container_stack.definitionChanges
|
||||||
|
machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
|
||||||
|
extruder_count = 0
|
||||||
|
for position, extruder in self._global_container_stack.extruders.items():
|
||||||
|
if extruder.isEnabled and int(position) < machine_extruder_count:
|
||||||
|
extruder_count += 1
|
||||||
|
if self.numberExtrudersEnabled != extruder_count:
|
||||||
|
definition_changes_container.setProperty("extruders_enabled_count", "value", extruder_count)
|
||||||
|
self.numberExtrudersEnabledChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify = numberExtrudersEnabledChanged)
|
||||||
|
def numberExtrudersEnabled(self):
|
||||||
|
return self._global_container_stack.definitionChanges.getProperty("extruders_enabled_count", "value")
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = extruderChanged)
|
||||||
|
def defaultExtruderPosition(self):
|
||||||
|
return self._default_extruder_position
|
||||||
|
|
||||||
|
## This will fire the propertiesChanged for all settings so they will be updated in the front-end
|
||||||
|
@pyqtSlot()
|
||||||
|
def forceUpdateAllSettings(self):
|
||||||
|
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
||||||
|
property_names = ["value", "resolve", "validationState"]
|
||||||
|
for container in [self._global_container_stack] + list(self._global_container_stack.extruders.values()):
|
||||||
|
for setting_key in container.getAllKeys():
|
||||||
|
container.propertiesChanged.emit(setting_key, property_names)
|
||||||
|
|
||||||
|
@pyqtSlot(int, bool)
|
||||||
|
def setExtruderEnabled(self, position: int, enabled) -> None:
|
||||||
|
extruder = self.getExtruder(position)
|
||||||
|
extruder.setEnabled(enabled)
|
||||||
|
self.updateDefaultExtruder()
|
||||||
|
self.updateNumberExtrudersEnabled()
|
||||||
|
self.correctExtruderSettings()
|
||||||
|
# ensure that the quality profile is compatible with current combination, or choose a compatible one if available
|
||||||
|
self._updateQualityWithMaterial()
|
||||||
|
self.extruderChanged.emit()
|
||||||
|
# update material compatibility color
|
||||||
|
self.activeQualityGroupChanged.emit()
|
||||||
|
# update items in SettingExtruder
|
||||||
|
ExtruderManager.getInstance().extrudersChanged.emit(self._global_container_stack.getId())
|
||||||
|
# Make sure the front end reflects changes
|
||||||
|
self.forceUpdateAllSettings()
|
||||||
|
|
||||||
def _onMachineNameChanged(self):
|
def _onMachineNameChanged(self):
|
||||||
self.globalContainerChanged.emit()
|
self.globalContainerChanged.emit()
|
||||||
|
|
||||||
|
@ -779,29 +924,24 @@ class MachineManager(QObject):
|
||||||
container = extruder.userChanges
|
container = extruder.userChanges
|
||||||
container.setProperty(setting_name, property_name, property_value)
|
container.setProperty(setting_name, property_name, property_value)
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify = rootMaterialChanged)
|
@pyqtProperty("QVariantList", notify = globalContainerChanged)
|
||||||
def currentExtruderPositions(self):
|
def currentExtruderPositions(self):
|
||||||
return sorted(list(self._current_root_material_id.keys()))
|
if self._global_container_stack is None:
|
||||||
|
return []
|
||||||
|
return sorted(list(self._global_container_stack.extruders.keys()))
|
||||||
|
|
||||||
|
## Update _current_root_material_id when the current root material was changed.
|
||||||
|
def _onRootMaterialChanged(self):
|
||||||
|
self._current_root_material_id = {}
|
||||||
|
|
||||||
|
if self._global_container_stack:
|
||||||
|
for position in self._global_container_stack.extruders:
|
||||||
|
self._current_root_material_id[position] = self._global_container_stack.extruders[position].material.getMetaDataEntry("base_file")
|
||||||
|
|
||||||
@pyqtProperty("QVariant", notify = rootMaterialChanged)
|
@pyqtProperty("QVariant", notify = rootMaterialChanged)
|
||||||
def currentRootMaterialId(self):
|
def currentRootMaterialId(self):
|
||||||
# initial filling the current_root_material_id
|
|
||||||
self._current_root_material_id = {}
|
|
||||||
for position in self._global_container_stack.extruders:
|
|
||||||
self._current_root_material_id[position] = self._global_container_stack.extruders[position].material.getMetaDataEntry("base_file")
|
|
||||||
return self._current_root_material_id
|
return self._current_root_material_id
|
||||||
|
|
||||||
@pyqtProperty("QVariant", notify = rootMaterialChanged)
|
|
||||||
def currentRootMaterialName(self):
|
|
||||||
# initial filling the current_root_material_name
|
|
||||||
if self._global_container_stack:
|
|
||||||
self._current_root_material_name = {}
|
|
||||||
for position in self._global_container_stack.extruders:
|
|
||||||
if position not in self._current_root_material_name:
|
|
||||||
material = self._global_container_stack.extruders[position].material
|
|
||||||
self._current_root_material_name[position] = material.getName()
|
|
||||||
return self._current_root_material_name
|
|
||||||
|
|
||||||
## Return the variant names in the extruder stack(s).
|
## Return the variant names in the extruder stack(s).
|
||||||
## For the variant in the global stack, use activeVariantBuildplateName
|
## For the variant in the global stack, use activeVariantBuildplateName
|
||||||
@pyqtProperty("QVariant", notify = activeVariantChanged)
|
@pyqtProperty("QVariant", notify = activeVariantChanged)
|
||||||
|
@ -846,9 +986,9 @@ class MachineManager(QObject):
|
||||||
|
|
||||||
# Set quality and quality_changes for each ExtruderStack
|
# Set quality and quality_changes for each ExtruderStack
|
||||||
for position, node in quality_group.nodes_for_extruders.items():
|
for position, node in quality_group.nodes_for_extruders.items():
|
||||||
self._global_container_stack.extruders[position].quality = node.getContainer()
|
self._global_container_stack.extruders[str(position)].quality = node.getContainer()
|
||||||
if empty_quality_changes:
|
if empty_quality_changes:
|
||||||
self._global_container_stack.extruders[position].qualityChanges = self._empty_quality_changes_container
|
self._global_container_stack.extruders[str(position)].qualityChanges = self._empty_quality_changes_container
|
||||||
|
|
||||||
self.activeQualityGroupChanged.emit()
|
self.activeQualityGroupChanged.emit()
|
||||||
self.activeQualityChangesGroupChanged.emit()
|
self.activeQualityChangesGroupChanged.emit()
|
||||||
|
@ -873,7 +1013,7 @@ class MachineManager(QObject):
|
||||||
quality_node = quality_group.nodes_for_extruders.get(position)
|
quality_node = quality_group.nodes_for_extruders.get(position)
|
||||||
|
|
||||||
quality_changes_container = self._empty_quality_changes_container
|
quality_changes_container = self._empty_quality_changes_container
|
||||||
quality_container = self._empty_quality_changes_container
|
quality_container = self._empty_quality_container
|
||||||
if quality_changes_node:
|
if quality_changes_node:
|
||||||
quality_changes_container = quality_changes_node.getContainer()
|
quality_changes_container = quality_changes_node.getContainer()
|
||||||
if quality_node:
|
if quality_node:
|
||||||
|
@ -897,50 +1037,59 @@ class MachineManager(QObject):
|
||||||
def _setMaterial(self, position, container_node = None):
|
def _setMaterial(self, position, container_node = None):
|
||||||
if container_node:
|
if container_node:
|
||||||
self._global_container_stack.extruders[position].material = container_node.getContainer()
|
self._global_container_stack.extruders[position].material = container_node.getContainer()
|
||||||
|
root_material_id = container_node.metadata["base_file"]
|
||||||
else:
|
else:
|
||||||
self._global_container_stack.extruders[position].material = self._empty_material_container
|
self._global_container_stack.extruders[position].material = self._empty_material_container
|
||||||
|
root_material_id = None
|
||||||
# The _current_root_material_id is used in the MaterialMenu to see which material is selected
|
# The _current_root_material_id is used in the MaterialMenu to see which material is selected
|
||||||
root_material_id = container_node.metadata["base_file"]
|
|
||||||
root_material_name = container_node.getContainer().getName()
|
|
||||||
if root_material_id != self._current_root_material_id[position]:
|
if root_material_id != self._current_root_material_id[position]:
|
||||||
self._current_root_material_id[position] = root_material_id
|
self._current_root_material_id[position] = root_material_id
|
||||||
self._current_root_material_name[position] = root_material_name
|
|
||||||
self.rootMaterialChanged.emit()
|
self.rootMaterialChanged.emit()
|
||||||
|
|
||||||
def activeMaterialsCompatible(self):
|
def activeMaterialsCompatible(self):
|
||||||
# check material - variant compatibility
|
# check material - variant compatibility
|
||||||
if Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False)):
|
if Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False)):
|
||||||
for position, extruder in self._global_container_stack.extruders.items():
|
for position, extruder in self._global_container_stack.extruders.items():
|
||||||
|
if extruder.isEnabled and not extruder.material.getMetaDataEntry("compatible"):
|
||||||
|
return False
|
||||||
if not extruder.material.getMetaDataEntry("compatible"):
|
if not extruder.material.getMetaDataEntry("compatible"):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
## Update current quality type and machine after setting material
|
## Update current quality type and machine after setting material
|
||||||
def _updateQualityWithMaterial(self):
|
def _updateQualityWithMaterial(self, *args):
|
||||||
current_quality = None
|
Logger.log("i", "Updating quality/quality_changes due to material change")
|
||||||
|
current_quality_type = None
|
||||||
if self._current_quality_group:
|
if self._current_quality_group:
|
||||||
current_quality = self._current_quality_group.quality_type
|
current_quality_type = self._current_quality_group.quality_type
|
||||||
quality_manager = Application.getInstance()._quality_manager
|
candidate_quality_groups = self._quality_manager.getQualityGroups(self._global_container_stack)
|
||||||
candidate_quality_groups = quality_manager.getQualityGroups(self._global_container_stack)
|
|
||||||
available_quality_types = {qt for qt, g in candidate_quality_groups.items() if g.is_available}
|
available_quality_types = {qt for qt, g in candidate_quality_groups.items() if g.is_available}
|
||||||
|
|
||||||
|
Logger.log("d", "Current quality type = [%s]", current_quality_type)
|
||||||
if not self.activeMaterialsCompatible():
|
if not self.activeMaterialsCompatible():
|
||||||
|
Logger.log("i", "Active materials are not compatible, setting all qualities to empty (Not Supported).")
|
||||||
self._setEmptyQuality()
|
self._setEmptyQuality()
|
||||||
return
|
return
|
||||||
|
|
||||||
if not available_quality_types:
|
if not available_quality_types:
|
||||||
|
Logger.log("i", "No available quality types found, setting all qualities to empty (Not Supported).")
|
||||||
self._setEmptyQuality()
|
self._setEmptyQuality()
|
||||||
return
|
return
|
||||||
|
|
||||||
if current_quality in available_quality_types:
|
if current_quality_type in available_quality_types:
|
||||||
self._setQualityGroup(candidate_quality_groups[current_quality], empty_quality_changes = False)
|
Logger.log("i", "Current available quality type [%s] is available, applying changes.", current_quality_type)
|
||||||
|
self._setQualityGroup(candidate_quality_groups[current_quality_type], empty_quality_changes = False)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# The current quality type is not available so we use the preferred quality type if it's available,
|
||||||
|
# otherwise use one of the available quality types.
|
||||||
quality_type = sorted(list(available_quality_types))[0]
|
quality_type = sorted(list(available_quality_types))[0]
|
||||||
preferred_quality_type = self._global_container_stack.getMetaDataEntry("preferred_quality_type")
|
preferred_quality_type = self._global_container_stack.getMetaDataEntry("preferred_quality_type")
|
||||||
if preferred_quality_type in available_quality_types:
|
if preferred_quality_type in available_quality_types:
|
||||||
quality_type = preferred_quality_type
|
quality_type = preferred_quality_type
|
||||||
|
|
||||||
|
Logger.log("i", "The current quality type [%s] is not available, switching to [%s] instead",
|
||||||
|
current_quality_type, quality_type)
|
||||||
self._setQualityGroup(candidate_quality_groups[quality_type], empty_quality_changes = True)
|
self._setQualityGroup(candidate_quality_groups[quality_type], empty_quality_changes = True)
|
||||||
|
|
||||||
def _updateMaterialWithVariant(self, position: Optional[str]):
|
def _updateMaterialWithVariant(self, position: Optional[str]):
|
||||||
|
@ -953,11 +1102,16 @@ class MachineManager(QObject):
|
||||||
extruder = self._global_container_stack.extruders[position]
|
extruder = self._global_container_stack.extruders[position]
|
||||||
|
|
||||||
current_material_base_name = extruder.material.getMetaDataEntry("base_file")
|
current_material_base_name = extruder.material.getMetaDataEntry("base_file")
|
||||||
|
current_variant_name = None
|
||||||
|
if extruder.variant.getId() != self._empty_variant_container.getId():
|
||||||
current_variant_name = extruder.variant.getMetaDataEntry("name")
|
current_variant_name = extruder.variant.getMetaDataEntry("name")
|
||||||
|
|
||||||
material_manager = Application.getInstance()._material_manager
|
from UM.Settings.Interfaces import PropertyEvaluationContext
|
||||||
material_diameter = self._global_container_stack.getProperty("material_diameter", "value")
|
from cura.Settings.CuraContainerStack import _ContainerIndexes
|
||||||
candidate_materials = material_manager.getAvailableMaterials(
|
context = PropertyEvaluationContext(extruder)
|
||||||
|
context.context["evaluate_from_container_index"] = _ContainerIndexes.DefinitionChanges
|
||||||
|
material_diameter = extruder.getProperty("material_diameter", "value", context)
|
||||||
|
candidate_materials = self._material_manager.getAvailableMaterials(
|
||||||
self._global_container_stack.definition.getId(),
|
self._global_container_stack.definition.getId(),
|
||||||
current_variant_name,
|
current_variant_name,
|
||||||
material_diameter)
|
material_diameter)
|
||||||
|
@ -971,6 +1125,92 @@ class MachineManager(QObject):
|
||||||
self._setMaterial(position, new_material)
|
self._setMaterial(position, new_material)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# The current material is not available, find the preferred one
|
||||||
|
material_node = self._material_manager.getDefaultMaterial(self._global_container_stack, current_variant_name)
|
||||||
|
if material_node is not None:
|
||||||
|
self._setMaterial(position, material_node)
|
||||||
|
|
||||||
|
## Given a printer definition name, select the right machine instance. In case it doesn't exist, create a new
|
||||||
|
# instance with the same network key.
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def switchPrinterType(self, machine_name):
|
||||||
|
# Don't switch if the user tries to change to the same type of printer
|
||||||
|
if self.activeMachineDefinitionName == machine_name:
|
||||||
|
return
|
||||||
|
# Get the definition id corresponding to this machine name
|
||||||
|
machine_definition_id = ContainerRegistry.getInstance().findDefinitionContainers(name = machine_name)[0].getId()
|
||||||
|
# Try to find a machine with the same network key
|
||||||
|
new_machine = self.getMachine(machine_definition_id, metadata_filter = {"um_network_key": self.activeMachineNetworkKey})
|
||||||
|
# If there is no machine, then create a new one and set it to the non-hidden instance
|
||||||
|
if not new_machine:
|
||||||
|
new_machine = CuraStackBuilder.createMachine(machine_definition_id + "_sync", machine_definition_id)
|
||||||
|
new_machine.addMetaDataEntry("um_network_key", self.activeMachineNetworkKey)
|
||||||
|
new_machine.addMetaDataEntry("connect_group_name", self.activeMachineNetworkGroupName)
|
||||||
|
new_machine.addMetaDataEntry("hidden", False)
|
||||||
|
else:
|
||||||
|
Logger.log("i", "Found a %s with the key %s. Let's use it!", machine_name, self.activeMachineNetworkKey)
|
||||||
|
new_machine.setMetaDataEntry("hidden", False)
|
||||||
|
|
||||||
|
# Set the current printer instance to hidden (the metadata entry must exist)
|
||||||
|
self._global_container_stack.setMetaDataEntry("hidden", True)
|
||||||
|
|
||||||
|
self.setActiveMachine(new_machine.getId())
|
||||||
|
|
||||||
|
@pyqtSlot(QObject)
|
||||||
|
def applyRemoteConfiguration(self, configuration: ConfigurationModel):
|
||||||
|
self.blurSettings.emit()
|
||||||
|
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
||||||
|
self.switchPrinterType(configuration.printerType)
|
||||||
|
for extruder_configuration in configuration.extruderConfigurations:
|
||||||
|
position = str(extruder_configuration.position)
|
||||||
|
variant_container_node = self._variant_manager.getVariantNode(self._global_container_stack.definition.getId(), extruder_configuration.hotendID)
|
||||||
|
material_container_node = self._material_manager.getMaterialNodeByType(self._global_container_stack, extruder_configuration.hotendID,extruder_configuration.material.guid)
|
||||||
|
if variant_container_node:
|
||||||
|
self._setVariantNode(position, variant_container_node)
|
||||||
|
else:
|
||||||
|
self._global_container_stack.extruders[position].variant = self._empty_variant_container
|
||||||
|
|
||||||
|
if material_container_node:
|
||||||
|
self._setMaterial(position, material_container_node)
|
||||||
|
else:
|
||||||
|
self._global_container_stack.extruders[position].material = self._empty_material_container
|
||||||
|
self._updateMaterialWithVariant(position)
|
||||||
|
|
||||||
|
if configuration.buildplateConfiguration is not None:
|
||||||
|
global_variant_container_node = self._variant_manager.getBuildplateVariantNode(self._global_container_stack.definition.getId(), configuration.buildplateConfiguration)
|
||||||
|
if global_variant_container_node:
|
||||||
|
self._setGlobalVariant(global_variant_container_node)
|
||||||
|
else:
|
||||||
|
self._global_container_stack.variant = self._empty_variant_container
|
||||||
|
else:
|
||||||
|
self._global_container_stack.variant = self._empty_variant_container
|
||||||
|
self._updateQualityWithMaterial()
|
||||||
|
|
||||||
|
## Find all container stacks that has the pair 'key = value' in its metadata and replaces the value with 'new_value'
|
||||||
|
def replaceContainersMetadata(self, key: str, value: str, new_value: str):
|
||||||
|
machines = ContainerRegistry.getInstance().findContainerStacks(type = "machine")
|
||||||
|
for machine in machines:
|
||||||
|
if machine.getMetaDataEntry(key) == value:
|
||||||
|
machine.setMetaDataEntry(key, new_value)
|
||||||
|
|
||||||
|
## This method checks if the name of the group stored in the definition container is correct.
|
||||||
|
# After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group
|
||||||
|
# then all the container stacks are updated, both the current and the hidden ones.
|
||||||
|
def checkCorrectGroupName(self, device_id: str, group_name: str):
|
||||||
|
if self._global_container_stack and device_id == self.activeMachineNetworkKey:
|
||||||
|
# Check if the connect_group_name is correct. If not, update all the containers connected to the same printer
|
||||||
|
if self.activeMachineNetworkGroupName != group_name:
|
||||||
|
metadata_filter = {"um_network_key": self.activeMachineNetworkKey}
|
||||||
|
hidden_containers = ContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
|
||||||
|
for container in hidden_containers:
|
||||||
|
container.setMetaDataEntry("connect_group_name", group_name)
|
||||||
|
|
||||||
|
## This method checks if there is an instance connected to the given network_key
|
||||||
|
def existNetworkInstances(self, network_key: str) -> bool:
|
||||||
|
metadata_filter = {"um_network_key": network_key}
|
||||||
|
containers = ContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
|
||||||
|
return bool(containers)
|
||||||
|
|
||||||
@pyqtSlot("QVariant")
|
@pyqtSlot("QVariant")
|
||||||
def setGlobalVariant(self, container_node):
|
def setGlobalVariant(self, container_node):
|
||||||
self.blurSettings.emit()
|
self.blurSettings.emit()
|
||||||
|
@ -979,6 +1219,16 @@ class MachineManager(QObject):
|
||||||
self._updateMaterialWithVariant(None) # Update all materials
|
self._updateMaterialWithVariant(None) # Update all materials
|
||||||
self._updateQualityWithMaterial()
|
self._updateQualityWithMaterial()
|
||||||
|
|
||||||
|
@pyqtSlot(str, str)
|
||||||
|
def setMaterialById(self, position, root_material_id):
|
||||||
|
machine_definition_id = self._global_container_stack.definition.id
|
||||||
|
position = str(position)
|
||||||
|
extruder_stack = self._global_container_stack.extruders[position]
|
||||||
|
variant_name = extruder_stack.variant.getName()
|
||||||
|
material_diameter = extruder_stack.approximateMaterialDiameter
|
||||||
|
material_node = self._material_manager.getMaterialNode(machine_definition_id, variant_name, material_diameter, root_material_id)
|
||||||
|
self.setMaterial(position, material_node)
|
||||||
|
|
||||||
@pyqtSlot(str, "QVariant")
|
@pyqtSlot(str, "QVariant")
|
||||||
def setMaterial(self, position, container_node):
|
def setMaterial(self, position, container_node):
|
||||||
position = str(position)
|
position = str(position)
|
||||||
|
@ -987,8 +1237,14 @@ class MachineManager(QObject):
|
||||||
self._setMaterial(position, container_node)
|
self._setMaterial(position, container_node)
|
||||||
self._updateQualityWithMaterial()
|
self._updateQualityWithMaterial()
|
||||||
|
|
||||||
|
@pyqtSlot(str, str)
|
||||||
|
def setVariantByName(self, position, variant_name):
|
||||||
|
machine_definition_id = self._global_container_stack.definition.id
|
||||||
|
variant_node = self._variant_manager.getVariantNode(machine_definition_id, variant_name)
|
||||||
|
self.setVariant(position, variant_node)
|
||||||
|
|
||||||
@pyqtSlot(str, "QVariant")
|
@pyqtSlot(str, "QVariant")
|
||||||
def setVariantGroup(self, position, container_node):
|
def setVariant(self, position, container_node):
|
||||||
position = str(position)
|
position = str(position)
|
||||||
self.blurSettings.emit()
|
self.blurSettings.emit()
|
||||||
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
||||||
|
@ -996,29 +1252,43 @@ class MachineManager(QObject):
|
||||||
self._updateMaterialWithVariant(position)
|
self._updateMaterialWithVariant(position)
|
||||||
self._updateQualityWithMaterial()
|
self._updateQualityWithMaterial()
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def setQualityGroupByQualityType(self, quality_type):
|
||||||
|
# Get all the quality groups for this global stack and filter out by quality_type
|
||||||
|
quality_group_dict = self._quality_manager.getQualityGroups(self._global_container_stack)
|
||||||
|
quality_group = quality_group_dict[quality_type]
|
||||||
|
self.setQualityGroup(quality_group)
|
||||||
|
|
||||||
@pyqtSlot(QObject)
|
@pyqtSlot(QObject)
|
||||||
def setQualityGroup(self, quality_group):
|
def setQualityGroup(self, quality_group, no_dialog = False):
|
||||||
self.blurSettings.emit()
|
self.blurSettings.emit()
|
||||||
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
||||||
self._setQualityGroup(quality_group)
|
self._setQualityGroup(quality_group)
|
||||||
|
|
||||||
# See if we need to show the Discard or Keep changes screen
|
# See if we need to show the Discard or Keep changes screen
|
||||||
if self.hasUserSettings and Preferences.getInstance().getValue("cura/active_mode") == 1:
|
if not no_dialog and self.hasUserSettings and Preferences.getInstance().getValue("cura/active_mode") == 1:
|
||||||
Application.getInstance().discardOrKeepProfileChanges()
|
self._application.discardOrKeepProfileChanges()
|
||||||
|
|
||||||
@pyqtProperty(QObject, fset = setQualityGroup, notify = activeQualityGroupChanged)
|
@pyqtProperty(QObject, fset = setQualityGroup, notify = activeQualityGroupChanged)
|
||||||
def activeQualityGroup(self):
|
def activeQualityGroup(self):
|
||||||
return self._current_quality_group
|
return self._current_quality_group
|
||||||
|
|
||||||
@pyqtSlot(QObject)
|
@pyqtSlot(QObject)
|
||||||
def setQualityChangesGroup(self, quality_changes_group):
|
def setQualityChangesGroup(self, quality_changes_group, no_dialog = False):
|
||||||
self.blurSettings.emit()
|
self.blurSettings.emit()
|
||||||
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
||||||
self._setQualityChangesGroup(quality_changes_group)
|
self._setQualityChangesGroup(quality_changes_group)
|
||||||
|
|
||||||
# See if we need to show the Discard or Keep changes screen
|
# See if we need to show the Discard or Keep changes screen
|
||||||
if self.hasUserSettings and Preferences.getInstance().getValue("cura/active_mode") == 1:
|
if not no_dialog and self.hasUserSettings and Preferences.getInstance().getValue("cura/active_mode") == 1:
|
||||||
Application.getInstance().discardOrKeepProfileChanges()
|
self._application.discardOrKeepProfileChanges()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def resetToUseDefaultQuality(self):
|
||||||
|
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
||||||
|
self._setQualityGroup(self._current_quality_group)
|
||||||
|
for stack in [self._global_container_stack] + list(self._global_container_stack.extruders.values()):
|
||||||
|
stack.userChanges.clear()
|
||||||
|
|
||||||
@pyqtProperty(QObject, fset = setQualityChangesGroup, notify = activeQualityChangesGroupChanged)
|
@pyqtProperty(QObject, fset = setQualityChangesGroup, notify = activeQualityChangesGroupChanged)
|
||||||
def activeQualityChangesGroup(self):
|
def activeQualityChangesGroup(self):
|
||||||
|
@ -1032,3 +1302,8 @@ class MachineManager(QObject):
|
||||||
elif self._current_quality_group:
|
elif self._current_quality_group:
|
||||||
name = self._current_quality_group.name
|
name = self._current_quality_group.name
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
def _updateUponMaterialMetadataChange(self):
|
||||||
|
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
||||||
|
self._updateMaterialWithVariant(None)
|
||||||
|
self._updateQualityWithMaterial()
|
||||||
|
|
|
@ -3,13 +3,14 @@ from typing import Any, Optional
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Decorators import override
|
from UM.Decorators import override
|
||||||
from UM.Settings.Interfaces import PropertyEvaluationContext
|
from UM.Settings.Interfaces import PropertyEvaluationContext
|
||||||
from UM.Settings.ContainerStack import ContainerStack
|
|
||||||
from UM.Settings.SettingInstance import InstanceState
|
from UM.Settings.SettingInstance import InstanceState
|
||||||
|
|
||||||
|
from .CuraContainerStack import CuraContainerStack
|
||||||
|
|
||||||
class PerObjectContainerStack(ContainerStack):
|
|
||||||
|
|
||||||
@override(ContainerStack)
|
class PerObjectContainerStack(CuraContainerStack):
|
||||||
|
|
||||||
|
@override(CuraContainerStack)
|
||||||
def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any:
|
def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any:
|
||||||
if context is None:
|
if context is None:
|
||||||
context = PropertyEvaluationContext()
|
context = PropertyEvaluationContext()
|
||||||
|
@ -51,8 +52,8 @@ class PerObjectContainerStack(ContainerStack):
|
||||||
context.popContainer()
|
context.popContainer()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@override(ContainerStack)
|
@override(CuraContainerStack)
|
||||||
def setNextStack(self, stack: ContainerStack):
|
def setNextStack(self, stack: CuraContainerStack):
|
||||||
super().setNextStack(stack)
|
super().setNextStack(stack)
|
||||||
|
|
||||||
# trigger signal to re-evaluate all default settings
|
# trigger signal to re-evaluate all default settings
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
|
from PyQt5.QtCore import QObject, QTimer, pyqtProperty, pyqtSignal
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
@ -30,6 +30,11 @@ class SettingInheritanceManager(QObject):
|
||||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderChanged)
|
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderChanged)
|
||||||
self._onActiveExtruderChanged()
|
self._onActiveExtruderChanged()
|
||||||
|
|
||||||
|
self._update_timer = QTimer()
|
||||||
|
self._update_timer.setInterval(500)
|
||||||
|
self._update_timer.setSingleShot(True)
|
||||||
|
self._update_timer.timeout.connect(self._update)
|
||||||
|
|
||||||
settingsWithIntheritanceChanged = pyqtSignal()
|
settingsWithIntheritanceChanged = pyqtSignal()
|
||||||
|
|
||||||
## Get the keys of all children settings with an override.
|
## Get the keys of all children settings with an override.
|
||||||
|
@ -226,9 +231,7 @@ class SettingInheritanceManager(QObject):
|
||||||
self._onActiveExtruderChanged()
|
self._onActiveExtruderChanged()
|
||||||
|
|
||||||
def _onContainersChanged(self, container):
|
def _onContainersChanged(self, container):
|
||||||
# TODO: Multiple container changes in sequence now cause quite a few recalculations.
|
self._update_timer.start()
|
||||||
# This isn't that big of an issue, but it could be in the future.
|
|
||||||
self._update()
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def createSettingInheritanceManager(engine=None, script_engine=None):
|
def createSettingInheritanceManager(engine=None, script_engine=None):
|
||||||
|
|
|
@ -3,14 +3,12 @@
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
|
||||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||||
from UM.Signal import Signal, signalemitter
|
from UM.Signal import Signal, signalemitter
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Settings.Validator import ValidatorState
|
|
||||||
from PyQt5.QtCore import QTimer
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
|
||||||
from cura.Settings.PerObjectContainerStack import PerObjectContainerStack
|
from cura.Settings.PerObjectContainerStack import PerObjectContainerStack
|
||||||
|
@ -34,16 +32,14 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._stack = PerObjectContainerStack(stack_id = "per_object_stack_" + str(id(self)))
|
self._stack = PerObjectContainerStack(container_id = "per_object_stack_" + str(id(self)))
|
||||||
self._stack.setDirty(False) # This stack does not need to be saved.
|
self._stack.setDirty(False) # This stack does not need to be saved.
|
||||||
self._stack.addContainer(InstanceContainer(container_id = "SettingOverrideInstanceContainer"))
|
user_container = InstanceContainer(container_id = "SettingOverrideInstanceContainer")
|
||||||
|
user_container.addMetaDataEntry("type", "user")
|
||||||
|
self._stack.userChanges = user_container
|
||||||
self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0).getId()
|
self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0).getId()
|
||||||
|
|
||||||
self._is_non_printing_mesh = False
|
self._is_non_printing_mesh = False
|
||||||
self._error_check_timer = QTimer()
|
|
||||||
self._error_check_timer.setInterval(250)
|
|
||||||
self._error_check_timer.setSingleShot(True)
|
|
||||||
self._error_check_timer.timeout.connect(self._checkStackForErrors)
|
|
||||||
|
|
||||||
self._stack.propertyChanged.connect(self._onSettingChanged)
|
self._stack.propertyChanged.connect(self._onSettingChanged)
|
||||||
|
|
||||||
|
@ -67,7 +63,7 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||||
|
|
||||||
# use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh"
|
# use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh"
|
||||||
# has not been updated yet.
|
# has not been updated yet.
|
||||||
deep_copy._is_non_printing_mesh = any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings)
|
deep_copy._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
|
||||||
|
|
||||||
return deep_copy
|
return deep_copy
|
||||||
|
|
||||||
|
@ -95,26 +91,17 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||||
def isNonPrintingMesh(self):
|
def isNonPrintingMesh(self):
|
||||||
return self._is_non_printing_mesh
|
return self._is_non_printing_mesh
|
||||||
|
|
||||||
|
def evaluateIsNonPrintingMesh(self):
|
||||||
|
return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings)
|
||||||
|
|
||||||
def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function
|
def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function
|
||||||
# Trigger slice/need slicing if the value has changed.
|
|
||||||
if property_name == "value":
|
if property_name == "value":
|
||||||
self._is_non_printing_mesh = any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings)
|
# Trigger slice/need slicing if the value has changed.
|
||||||
if not self._is_non_printing_mesh:
|
self._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
|
||||||
# self._error_check_timer.start()
|
|
||||||
self._checkStackForErrors()
|
|
||||||
Application.getInstance().getBackend().needsSlicing()
|
Application.getInstance().getBackend().needsSlicing()
|
||||||
Application.getInstance().getBackend().tickle()
|
Application.getInstance().getBackend().tickle()
|
||||||
|
|
||||||
def _checkStackForErrors(self):
|
|
||||||
hasErrors = False;
|
|
||||||
for key in self._stack.getAllKeys():
|
|
||||||
validation_state = self._stack.getProperty(key, "validationState")
|
|
||||||
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
|
|
||||||
Logger.log("w", "Setting Per Object %s is not valid.", key)
|
|
||||||
hasErrors = True
|
|
||||||
break
|
|
||||||
Application.getInstance().getObjectsModel().setStacksHaveErrors(hasErrors)
|
|
||||||
|
|
||||||
## Makes sure that the stack upon which the container stack is placed is
|
## Makes sure that the stack upon which the container stack is placed is
|
||||||
# kept up to date.
|
# kept up to date.
|
||||||
def _updateNextStack(self):
|
def _updateNextStack(self):
|
||||||
|
|
|
@ -16,7 +16,8 @@ class SimpleModeSettingsManager(QObject):
|
||||||
self._is_profile_user_created = False # True when profile was custom created by user
|
self._is_profile_user_created = False # True when profile was custom created by user
|
||||||
|
|
||||||
self._machine_manager.activeStackValueChanged.connect(self._updateIsProfileCustomized)
|
self._machine_manager.activeStackValueChanged.connect(self._updateIsProfileCustomized)
|
||||||
self._machine_manager.activeQualityChanged.connect(self._updateIsProfileUserCreated)
|
self._machine_manager.activeQualityGroupChanged.connect(self._updateIsProfileUserCreated)
|
||||||
|
self._machine_manager.activeQualityChangesGroupChanged.connect(self._updateIsProfileUserCreated)
|
||||||
|
|
||||||
# update on create as the activeQualityChanged signal is emitted before this manager is created when Cura starts
|
# update on create as the activeQualityChanged signal is emitted before this manager is created when Cura starts
|
||||||
self._updateIsProfileCustomized()
|
self._updateIsProfileCustomized()
|
||||||
|
|
|
@ -66,7 +66,7 @@ class Snapshot:
|
||||||
size = max(bbox.width, bbox.height, bbox.depth * 0.5)
|
size = max(bbox.width, bbox.height, bbox.depth * 0.5)
|
||||||
|
|
||||||
# Looking from this direction (x, y, z) in OGL coordinates
|
# Looking from this direction (x, y, z) in OGL coordinates
|
||||||
looking_from_offset = Vector(1, 1, 2)
|
looking_from_offset = Vector(-1, 1, 2)
|
||||||
if size > 0:
|
if size > 0:
|
||||||
# determine the watch distance depending on the size
|
# determine the watch distance depending on the size
|
||||||
looking_from_offset = looking_from_offset * size * 1.3
|
looking_from_offset = looking_from_offset * size * 1.3
|
||||||
|
|
|
@ -16,7 +16,6 @@ from UM.Mesh.MeshBuilder import MeshBuilder
|
||||||
from UM.Mesh.MeshReader import MeshReader
|
from UM.Mesh.MeshReader import MeshReader
|
||||||
from UM.Scene.GroupDecorator import GroupDecorator
|
from UM.Scene.GroupDecorator import GroupDecorator
|
||||||
|
|
||||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||||
|
@ -81,7 +80,7 @@ class ThreeMFReader(MeshReader):
|
||||||
|
|
||||||
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
||||||
|
|
||||||
um_node = CuraSceneNode()
|
um_node = CuraSceneNode() # This adds a SettingOverrideDecorator
|
||||||
um_node.addDecorator(BuildPlateDecorator(active_build_plate))
|
um_node.addDecorator(BuildPlateDecorator(active_build_plate))
|
||||||
um_node.setName(node_name)
|
um_node.setName(node_name)
|
||||||
transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
|
transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
|
||||||
|
@ -110,8 +109,6 @@ class ThreeMFReader(MeshReader):
|
||||||
|
|
||||||
# Add the setting override decorator, so we can add settings to this node.
|
# Add the setting override decorator, so we can add settings to this node.
|
||||||
if settings:
|
if settings:
|
||||||
um_node.addDecorator(SettingOverrideDecorator())
|
|
||||||
|
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
|
||||||
# Ensure the correct next container for the SettingOverride decorator is set.
|
# Ensure the correct next container for the SettingOverride decorator is set.
|
||||||
|
@ -122,7 +119,7 @@ class ThreeMFReader(MeshReader):
|
||||||
um_node.callDecoration("setActiveExtruder", default_stack.getId())
|
um_node.callDecoration("setActiveExtruder", default_stack.getId())
|
||||||
|
|
||||||
# Get the definition & set it
|
# Get the definition & set it
|
||||||
definition_id = getMachineDefinitionIDForQualitySearch(global_container_stack)
|
definition_id = getMachineDefinitionIDForQualitySearch(global_container_stack.definition)
|
||||||
um_node.callDecoration("getStack").getTop().setDefinition(definition_id)
|
um_node.callDecoration("getStack").getTop().setDefinition(definition_id)
|
||||||
|
|
||||||
setting_container = um_node.callDecoration("getStack").getTop()
|
setting_container = um_node.callDecoration("getStack").getTop()
|
||||||
|
@ -140,7 +137,7 @@ class ThreeMFReader(MeshReader):
|
||||||
continue
|
continue
|
||||||
setting_container.setProperty(key, "value", setting_value)
|
setting_container.setProperty(key, "value", setting_value)
|
||||||
|
|
||||||
if len(um_node.getChildren()) > 0:
|
if len(um_node.getChildren()) > 0 and um_node.getMeshData() is None:
|
||||||
group_decorator = GroupDecorator()
|
group_decorator = GroupDecorator()
|
||||||
um_node.addDecorator(group_decorator)
|
um_node.addDecorator(group_decorator)
|
||||||
um_node.setSelectable(True)
|
um_node.setSelectable(True)
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -49,10 +49,10 @@ class WorkspaceDialog(QObject):
|
||||||
self._material_labels = []
|
self._material_labels = []
|
||||||
self._extruders = []
|
self._extruders = []
|
||||||
self._objects_on_plate = False
|
self._objects_on_plate = False
|
||||||
|
self._is_printer_group = False
|
||||||
|
|
||||||
machineConflictChanged = pyqtSignal()
|
machineConflictChanged = pyqtSignal()
|
||||||
qualityChangesConflictChanged = pyqtSignal()
|
qualityChangesConflictChanged = pyqtSignal()
|
||||||
definitionChangesConflictChanged = pyqtSignal()
|
|
||||||
materialConflictChanged = pyqtSignal()
|
materialConflictChanged = pyqtSignal()
|
||||||
numVisibleSettingsChanged = pyqtSignal()
|
numVisibleSettingsChanged = pyqtSignal()
|
||||||
activeModeChanged = pyqtSignal()
|
activeModeChanged = pyqtSignal()
|
||||||
|
@ -67,6 +67,16 @@ class WorkspaceDialog(QObject):
|
||||||
machineTypeChanged = pyqtSignal()
|
machineTypeChanged = pyqtSignal()
|
||||||
variantTypeChanged = pyqtSignal()
|
variantTypeChanged = pyqtSignal()
|
||||||
extrudersChanged = pyqtSignal()
|
extrudersChanged = pyqtSignal()
|
||||||
|
isPrinterGroupChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = isPrinterGroupChanged)
|
||||||
|
def isPrinterGroup(self) -> bool:
|
||||||
|
return self._is_printer_group
|
||||||
|
|
||||||
|
def setIsPrinterGroup(self, value: bool):
|
||||||
|
if value != self._is_printer_group:
|
||||||
|
self._is_printer_group = value
|
||||||
|
self.isPrinterGroupChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(str, notify=variantTypeChanged)
|
@pyqtProperty(str, notify=variantTypeChanged)
|
||||||
def variantType(self):
|
def variantType(self):
|
||||||
|
@ -196,10 +206,6 @@ class WorkspaceDialog(QObject):
|
||||||
def qualityChangesConflict(self):
|
def qualityChangesConflict(self):
|
||||||
return self._has_quality_changes_conflict
|
return self._has_quality_changes_conflict
|
||||||
|
|
||||||
@pyqtProperty(bool, notify=definitionChangesConflictChanged)
|
|
||||||
def definitionChangesConflict(self):
|
|
||||||
return self._has_definition_changes_conflict
|
|
||||||
|
|
||||||
@pyqtProperty(bool, notify=materialConflictChanged)
|
@pyqtProperty(bool, notify=materialConflictChanged)
|
||||||
def materialConflict(self):
|
def materialConflict(self):
|
||||||
return self._has_material_conflict
|
return self._has_material_conflict
|
||||||
|
@ -229,18 +235,11 @@ class WorkspaceDialog(QObject):
|
||||||
self._has_quality_changes_conflict = quality_changes_conflict
|
self._has_quality_changes_conflict = quality_changes_conflict
|
||||||
self.qualityChangesConflictChanged.emit()
|
self.qualityChangesConflictChanged.emit()
|
||||||
|
|
||||||
def setDefinitionChangesConflict(self, definition_changes_conflict):
|
|
||||||
if self._has_definition_changes_conflict != definition_changes_conflict:
|
|
||||||
self._has_definition_changes_conflict = definition_changes_conflict
|
|
||||||
self.definitionChangesConflictChanged.emit()
|
|
||||||
|
|
||||||
def getResult(self):
|
def getResult(self):
|
||||||
if "machine" in self._result and not self._has_machine_conflict:
|
if "machine" in self._result and not self._has_machine_conflict:
|
||||||
self._result["machine"] = None
|
self._result["machine"] = None
|
||||||
if "quality_changes" in self._result and not self._has_quality_changes_conflict:
|
if "quality_changes" in self._result and not self._has_quality_changes_conflict:
|
||||||
self._result["quality_changes"] = None
|
self._result["quality_changes"] = None
|
||||||
if "definition_changes" in self._result and not self._has_definition_changes_conflict:
|
|
||||||
self._result["definition_changes"] = None
|
|
||||||
if "material" in self._result and not self._has_material_conflict:
|
if "material" in self._result and not self._has_material_conflict:
|
||||||
self._result["material"] = None
|
self._result["material"] = None
|
||||||
|
|
||||||
|
|
|
@ -108,7 +108,22 @@ UM.Dialog
|
||||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?")
|
text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?")
|
||||||
ComboBox
|
ComboBox
|
||||||
{
|
{
|
||||||
model: resolveStrategiesModel
|
model: ListModel
|
||||||
|
{
|
||||||
|
Component.onCompleted:
|
||||||
|
{
|
||||||
|
append({"key": "override", "label": catalog.i18nc("@action:ComboBox option", "Update") + " " + manager.machineName});
|
||||||
|
append({"key": "new", "label": catalog.i18nc("@action:ComboBox option", "Create new")});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Connections
|
||||||
|
{
|
||||||
|
target: manager
|
||||||
|
onMachineNameChanged:
|
||||||
|
{
|
||||||
|
machineResolveComboBox.model.get(0).label = catalog.i18nc("@action:ComboBox option", "Update") + " " + manager.machineName;
|
||||||
|
}
|
||||||
|
}
|
||||||
textRole: "label"
|
textRole: "label"
|
||||||
id: machineResolveComboBox
|
id: machineResolveComboBox
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
@ -141,7 +156,7 @@ UM.Dialog
|
||||||
height: childrenRect.height
|
height: childrenRect.height
|
||||||
Label
|
Label
|
||||||
{
|
{
|
||||||
text: catalog.i18nc("@action:label", "Name")
|
text: catalog.i18nc("@action:label", manager.isPrinterGroup ? "Printer Group" : "Printer Name")
|
||||||
width: (parent.width / 3) | 0
|
width: (parent.width / 3) | 0
|
||||||
}
|
}
|
||||||
Label
|
Label
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Workspace.WorkspaceWriter import WorkspaceWriter
|
import configparser
|
||||||
|
from io import StringIO
|
||||||
|
import zipfile
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
from UM.Logger import Logger
|
||||||
from UM.Preferences import Preferences
|
from UM.Preferences import Preferences
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from UM.Workspace.WorkspaceWriter import WorkspaceWriter
|
||||||
import zipfile
|
|
||||||
from io import StringIO
|
|
||||||
import configparser
|
|
||||||
|
|
||||||
|
|
||||||
class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||||
|
@ -16,7 +17,10 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode):
|
def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode):
|
||||||
mesh_writer = Application.getInstance().getMeshFileHandler().getWriter("3MFWriter")
|
application = Application.getInstance()
|
||||||
|
machine_manager = application.getMachineManager()
|
||||||
|
|
||||||
|
mesh_writer = application.getMeshFileHandler().getWriter("3MFWriter")
|
||||||
|
|
||||||
if not mesh_writer: # We need to have the 3mf mesh writer, otherwise we can't save the entire workspace
|
if not mesh_writer: # We need to have the 3mf mesh writer, otherwise we can't save the entire workspace
|
||||||
return False
|
return False
|
||||||
|
@ -29,17 +33,17 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||||
if archive is None: # This happens if there was no mesh data to write.
|
if archive is None: # This happens if there was no mesh data to write.
|
||||||
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
|
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
|
||||||
|
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_stack = machine_manager.activeMachine
|
||||||
|
|
||||||
# Add global container stack data to the archive.
|
# Add global container stack data to the archive.
|
||||||
self._writeContainerToArchive(global_container_stack, archive)
|
self._writeContainerToArchive(global_stack, archive)
|
||||||
|
|
||||||
# Also write all containers in the stack to the file
|
# Also write all containers in the stack to the file
|
||||||
for container in global_container_stack.getContainers():
|
for container in global_stack.getContainers():
|
||||||
self._writeContainerToArchive(container, archive)
|
self._writeContainerToArchive(container, archive)
|
||||||
|
|
||||||
# Check if the machine has extruders and save all that data as well.
|
# Check if the machine has extruders and save all that data as well.
|
||||||
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()):
|
for extruder_stack in global_stack.extruders.values():
|
||||||
self._writeContainerToArchive(extruder_stack, archive)
|
self._writeContainerToArchive(extruder_stack, archive)
|
||||||
for container in extruder_stack.getContainers():
|
for container in extruder_stack.getContainers():
|
||||||
self._writeContainerToArchive(container, archive)
|
self._writeContainerToArchive(container, archive)
|
||||||
|
@ -59,9 +63,9 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||||
version_file = zipfile.ZipInfo("Cura/version.ini")
|
version_file = zipfile.ZipInfo("Cura/version.ini")
|
||||||
version_config_parser = configparser.ConfigParser(interpolation = None)
|
version_config_parser = configparser.ConfigParser(interpolation = None)
|
||||||
version_config_parser.add_section("versions")
|
version_config_parser.add_section("versions")
|
||||||
version_config_parser.set("versions", "cura_version", Application.getInstance().getVersion())
|
version_config_parser.set("versions", "cura_version", application.getVersion())
|
||||||
version_config_parser.set("versions", "build_type", Application.getInstance().getBuildType())
|
version_config_parser.set("versions", "build_type", application.getBuildType())
|
||||||
version_config_parser.set("versions", "is_debug_mode", str(Application.getInstance().getIsDebugMode()))
|
version_config_parser.set("versions", "is_debug_mode", str(application.getIsDebugMode()))
|
||||||
|
|
||||||
version_file_string = StringIO()
|
version_file_string = StringIO()
|
||||||
version_config_parser.write(version_file_string)
|
version_config_parser.write(version_file_string)
|
||||||
|
@ -85,6 +89,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||||
# Some containers have a base file, which should then be the file to use.
|
# Some containers have a base file, which should then be the file to use.
|
||||||
if "base_file" in container.getMetaData():
|
if "base_file" in container.getMetaData():
|
||||||
base_file = container.getMetaDataEntry("base_file")
|
base_file = container.getMetaDataEntry("base_file")
|
||||||
|
if base_file != container.getId():
|
||||||
container = ContainerRegistry.getInstance().findContainers(id = base_file)[0]
|
container = ContainerRegistry.getInstance().findContainers(id = base_file)[0]
|
||||||
|
|
||||||
file_name = "Cura/%s.%s" % (container.getId(), file_suffix)
|
file_name = "Cura/%s.%s" % (container.getId(), file_suffix)
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
[3.2.1]
|
||||||
|
*Bug fixes
|
||||||
|
- Fixed issues where Cura crashes on startup and loading profiles
|
||||||
|
- Updated translations
|
||||||
|
- Fixed an issue where the text would not render properly
|
||||||
|
|
||||||
[3.2.0]
|
[3.2.0]
|
||||||
*Tree support
|
*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.
|
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.
|
||||||
|
|
|
@ -10,7 +10,6 @@ from UM.Logger import Logger
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
from UM.PluginRegistry import PluginRegistry
|
from UM.PluginRegistry import PluginRegistry
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
from UM.Settings.Validator import ValidatorState #To find if a setting is in an error state. We can't slice then.
|
|
||||||
from UM.Platform import Platform
|
from UM.Platform import Platform
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
from UM.Qt.Duration import DurationFormat
|
from UM.Qt.Duration import DurationFormat
|
||||||
|
@ -32,7 +31,11 @@ import Arcus
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
class CuraEngineBackend(QObject, Backend):
|
class CuraEngineBackend(QObject, Backend):
|
||||||
|
|
||||||
|
backendError = Signal()
|
||||||
|
|
||||||
## Starts the back-end plug-in.
|
## Starts the back-end plug-in.
|
||||||
#
|
#
|
||||||
# This registers all the signal listeners and prepares for communication
|
# This registers all the signal listeners and prepares for communication
|
||||||
|
@ -59,23 +62,26 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
default_engine_location = execpath
|
default_engine_location = execpath
|
||||||
break
|
break
|
||||||
|
|
||||||
|
self._application = Application.getInstance()
|
||||||
|
self._multi_build_plate_model = None
|
||||||
|
self._machine_error_checker = None
|
||||||
|
|
||||||
if not default_engine_location:
|
if not default_engine_location:
|
||||||
raise EnvironmentError("Could not find CuraEngine")
|
raise EnvironmentError("Could not find CuraEngine")
|
||||||
|
|
||||||
Logger.log("i", "Found CuraEngine at: %s" %(default_engine_location))
|
Logger.log("i", "Found CuraEngine at: %s", default_engine_location)
|
||||||
|
|
||||||
default_engine_location = os.path.abspath(default_engine_location)
|
default_engine_location = os.path.abspath(default_engine_location)
|
||||||
Preferences.getInstance().addPreference("backend/location", default_engine_location)
|
Preferences.getInstance().addPreference("backend/location", default_engine_location)
|
||||||
|
|
||||||
# Workaround to disable layer view processing if layer view is not active.
|
# Workaround to disable layer view processing if layer view is not active.
|
||||||
self._layer_view_active = False
|
self._layer_view_active = False
|
||||||
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
|
|
||||||
Application.getInstance().getMultiBuildPlateModel().activeBuildPlateChanged.connect(self._onActiveViewChanged)
|
|
||||||
self._onActiveViewChanged()
|
self._onActiveViewChanged()
|
||||||
|
|
||||||
self._stored_layer_data = []
|
self._stored_layer_data = []
|
||||||
self._stored_optimized_layer_data = {} # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
|
self._stored_optimized_layer_data = {} # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
|
||||||
|
|
||||||
self._scene = Application.getInstance().getController().getScene()
|
self._scene = self._application.getController().getScene()
|
||||||
self._scene.sceneChanged.connect(self._onSceneChanged)
|
self._scene.sceneChanged.connect(self._onSceneChanged)
|
||||||
|
|
||||||
# Triggers for auto-slicing. Auto-slicing is triggered as follows:
|
# Triggers for auto-slicing. Auto-slicing is triggered as follows:
|
||||||
|
@ -83,18 +89,10 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
# - whenever there is a value change, we start the timer
|
# - whenever there is a value change, we start the timer
|
||||||
# - sometimes an error check can get scheduled for a value change, in that case, we ONLY want to start the
|
# - sometimes an error check can get scheduled for a value change, in that case, we ONLY want to start the
|
||||||
# auto-slicing timer when that error check is finished
|
# auto-slicing timer when that error check is finished
|
||||||
# If there is an error check, it will set the "_is_error_check_scheduled" flag, stop the auto-slicing timer,
|
# If there is an error check, stop the auto-slicing timer, and only wait for the error check to be finished
|
||||||
# and only wait for the error check to be finished to start the auto-slicing timer again.
|
# to start the auto-slicing timer again.
|
||||||
#
|
#
|
||||||
self._global_container_stack = None
|
self._global_container_stack = None
|
||||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
|
||||||
self._onGlobalStackChanged()
|
|
||||||
|
|
||||||
Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished)
|
|
||||||
|
|
||||||
# A flag indicating if an error check was scheduled
|
|
||||||
# If so, we will stop the auto-slice timer and start upon the error check
|
|
||||||
self._is_error_check_scheduled = False
|
|
||||||
|
|
||||||
# Listeners for receiving messages from the back-end.
|
# Listeners for receiving messages from the back-end.
|
||||||
self._message_handlers["cura.proto.Layer"] = self._onLayerMessage
|
self._message_handlers["cura.proto.Layer"] = self._onLayerMessage
|
||||||
|
@ -120,13 +118,6 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self._last_num_objects = defaultdict(int) # Count number of objects to see if there is something changed
|
self._last_num_objects = defaultdict(int) # Count number of objects to see if there is something changed
|
||||||
self._postponed_scene_change_sources = [] # scene change is postponed (by a tool)
|
self._postponed_scene_change_sources = [] # scene change is postponed (by a tool)
|
||||||
|
|
||||||
self.backendQuit.connect(self._onBackendQuit)
|
|
||||||
self.backendConnected.connect(self._onBackendConnected)
|
|
||||||
|
|
||||||
# When a tool operation is in progress, don't slice. So we need to listen for tool operations.
|
|
||||||
Application.getInstance().getController().toolOperationStarted.connect(self._onToolOperationStarted)
|
|
||||||
Application.getInstance().getController().toolOperationStopped.connect(self._onToolOperationStopped)
|
|
||||||
|
|
||||||
self._slice_start_time = None
|
self._slice_start_time = None
|
||||||
|
|
||||||
Preferences.getInstance().addPreference("general/auto_slice", True)
|
Preferences.getInstance().addPreference("general/auto_slice", True)
|
||||||
|
@ -141,6 +132,30 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self.determineAutoSlicing()
|
self.determineAutoSlicing()
|
||||||
Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
|
Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
|
||||||
|
|
||||||
|
self._application.initializationFinished.connect(self.initialize)
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
self._multi_build_plate_model = self._application.getMultiBuildPlateModel()
|
||||||
|
|
||||||
|
self._application.getController().activeViewChanged.connect(self._onActiveViewChanged)
|
||||||
|
self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveViewChanged)
|
||||||
|
|
||||||
|
self._application.globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
||||||
|
self._onGlobalStackChanged()
|
||||||
|
|
||||||
|
# extruder enable / disable. Actually wanted to use machine manager here, but the initialization order causes it to crash
|
||||||
|
ExtruderManager.getInstance().extrudersChanged.connect(self._extruderChanged)
|
||||||
|
|
||||||
|
self.backendQuit.connect(self._onBackendQuit)
|
||||||
|
self.backendConnected.connect(self._onBackendConnected)
|
||||||
|
|
||||||
|
# When a tool operation is in progress, don't slice. So we need to listen for tool operations.
|
||||||
|
self._application.getController().toolOperationStarted.connect(self._onToolOperationStarted)
|
||||||
|
self._application.getController().toolOperationStopped.connect(self._onToolOperationStopped)
|
||||||
|
|
||||||
|
self._machine_error_checker = self._application.getMachineErrorChecker()
|
||||||
|
self._machine_error_checker.errorCheckFinished.connect(self._onStackErrorCheckFinished)
|
||||||
|
|
||||||
## Terminate the engine process.
|
## Terminate the engine process.
|
||||||
#
|
#
|
||||||
# This function should terminate the engine process.
|
# This function should terminate the engine process.
|
||||||
|
@ -209,7 +224,11 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
||||||
build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0)
|
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)
|
Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced)
|
||||||
num_objects = self._numObjects()
|
num_objects = self._numObjectsPerBuildPlate()
|
||||||
|
|
||||||
|
self._stored_layer_data = []
|
||||||
|
self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
|
||||||
|
|
||||||
if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0:
|
if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0:
|
||||||
self._scene.gcode_dict[build_plate_to_be_sliced] = []
|
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)
|
Logger.log("d", "Build plate %s has no objects to be sliced, skipping", build_plate_to_be_sliced)
|
||||||
|
@ -217,9 +236,6 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self.slice()
|
self.slice()
|
||||||
return
|
return
|
||||||
|
|
||||||
self._stored_layer_data = []
|
|
||||||
self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
|
|
||||||
|
|
||||||
if Application.getInstance().getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
|
if Application.getInstance().getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
|
||||||
Application.getInstance().getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
|
Application.getInstance().getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
|
||||||
|
|
||||||
|
@ -289,6 +305,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
|
|
||||||
if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error:
|
if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error:
|
||||||
self.backendStateChange.emit(BackendState.Error)
|
self.backendStateChange.emit(BackendState.Error)
|
||||||
|
self.backendError.emit(job)
|
||||||
return
|
return
|
||||||
|
|
||||||
if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible:
|
if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible:
|
||||||
|
@ -297,6 +314,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
"Unable to slice with the current material as it is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Unable to slice"))
|
"Unable to slice with the current material as it is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
self.backendStateChange.emit(BackendState.Error)
|
self.backendStateChange.emit(BackendState.Error)
|
||||||
|
self.backendError.emit(job)
|
||||||
else:
|
else:
|
||||||
self.backendStateChange.emit(BackendState.NotStarted)
|
self.backendStateChange.emit(BackendState.NotStarted)
|
||||||
return
|
return
|
||||||
|
@ -325,6 +343,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
self.backendStateChange.emit(BackendState.Error)
|
self.backendStateChange.emit(BackendState.Error)
|
||||||
|
self.backendError.emit(job)
|
||||||
else:
|
else:
|
||||||
self.backendStateChange.emit(BackendState.NotStarted)
|
self.backendStateChange.emit(BackendState.NotStarted)
|
||||||
return
|
return
|
||||||
|
@ -347,6 +366,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
self.backendStateChange.emit(BackendState.Error)
|
self.backendStateChange.emit(BackendState.Error)
|
||||||
|
self.backendError.emit(job)
|
||||||
return
|
return
|
||||||
|
|
||||||
if job.getResult() == StartSliceJob.StartJobResult.BuildPlateError:
|
if job.getResult() == StartSliceJob.StartJobResult.BuildPlateError:
|
||||||
|
@ -355,6 +375,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
self.backendStateChange.emit(BackendState.Error)
|
self.backendStateChange.emit(BackendState.Error)
|
||||||
|
self.backendError.emit(job)
|
||||||
else:
|
else:
|
||||||
self.backendStateChange.emit(BackendState.NotStarted)
|
self.backendStateChange.emit(BackendState.NotStarted)
|
||||||
|
|
||||||
|
@ -364,6 +385,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
self.backendStateChange.emit(BackendState.Error)
|
self.backendStateChange.emit(BackendState.Error)
|
||||||
|
self.backendError.emit(job)
|
||||||
else:
|
else:
|
||||||
self.backendStateChange.emit(BackendState.NotStarted)
|
self.backendStateChange.emit(BackendState.NotStarted)
|
||||||
self._invokeSlice()
|
self._invokeSlice()
|
||||||
|
@ -405,7 +427,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
## Return a dict with number of objects per build plate
|
## Return a dict with number of objects per build plate
|
||||||
def _numObjects(self):
|
def _numObjectsPerBuildPlate(self):
|
||||||
num_objects = defaultdict(int)
|
num_objects = defaultdict(int)
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
# Only count sliceable objects
|
# Only count sliceable objects
|
||||||
|
@ -432,7 +454,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
source_build_plate_number = source.callDecoration("getBuildPlateNumber")
|
source_build_plate_number = source.callDecoration("getBuildPlateNumber")
|
||||||
if source == self._scene.getRoot():
|
if source == self._scene.getRoot():
|
||||||
# we got the root node
|
# we got the root node
|
||||||
num_objects = self._numObjects()
|
num_objects = self._numObjectsPerBuildPlate()
|
||||||
for build_plate_number in list(self._last_num_objects.keys()) + list(num_objects.keys()):
|
for build_plate_number in list(self._last_num_objects.keys()) + list(num_objects.keys()):
|
||||||
if build_plate_number not in self._last_num_objects or num_objects[build_plate_number] != self._last_num_objects[build_plate_number]:
|
if build_plate_number not in self._last_num_objects or num_objects[build_plate_number] != self._last_num_objects[build_plate_number]:
|
||||||
self._last_num_objects[build_plate_number] = num_objects[build_plate_number]
|
self._last_num_objects[build_plate_number] = num_objects[build_plate_number]
|
||||||
|
@ -520,11 +542,9 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
|
|
||||||
elif property == "validationState":
|
elif property == "validationState":
|
||||||
if self._use_timer:
|
if self._use_timer:
|
||||||
self._is_error_check_scheduled = True
|
|
||||||
self._change_timer.stop()
|
self._change_timer.stop()
|
||||||
|
|
||||||
def _onStackErrorCheckFinished(self):
|
def _onStackErrorCheckFinished(self):
|
||||||
self._is_error_check_scheduled = False
|
|
||||||
if not self._slicing and self._build_plates_to_be_sliced:
|
if not self._slicing and self._build_plates_to_be_sliced:
|
||||||
self.needsSlicing()
|
self.needsSlicing()
|
||||||
self._onChanged()
|
self._onChanged()
|
||||||
|
@ -550,12 +570,15 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self.processingProgress.emit(message.amount)
|
self.processingProgress.emit(message.amount)
|
||||||
self.backendStateChange.emit(BackendState.Processing)
|
self.backendStateChange.emit(BackendState.Processing)
|
||||||
|
|
||||||
# testing
|
|
||||||
def _invokeSlice(self):
|
def _invokeSlice(self):
|
||||||
if self._use_timer:
|
if self._use_timer:
|
||||||
# if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
|
# if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
|
||||||
# otherwise business as usual
|
# otherwise business as usual
|
||||||
if self._is_error_check_scheduled:
|
if self._machine_error_checker is None:
|
||||||
|
self._change_timer.stop()
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._machine_error_checker.needToWaitForResult:
|
||||||
self._change_timer.stop()
|
self._change_timer.stop()
|
||||||
else:
|
else:
|
||||||
self._change_timer.start()
|
self._change_timer.start()
|
||||||
|
@ -582,7 +605,12 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
|
|
||||||
# See if we need to process the sliced layers job.
|
# See if we need to process the sliced layers job.
|
||||||
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
||||||
if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()) and active_build_plate == self._start_slice_job_build_plate:
|
if (
|
||||||
|
self._layer_view_active and
|
||||||
|
(self._process_layers_job is None or not self._process_layers_job.isRunning()) and
|
||||||
|
active_build_plate == self._start_slice_job_build_plate and
|
||||||
|
active_build_plate not in self._build_plates_to_be_sliced):
|
||||||
|
|
||||||
self._startProcessSlicedLayersJob(active_build_plate)
|
self._startProcessSlicedLayersJob(active_build_plate)
|
||||||
# self._onActiveViewChanged()
|
# self._onActiveViewChanged()
|
||||||
self._start_slice_job_build_plate = None
|
self._start_slice_job_build_plate = None
|
||||||
|
@ -621,7 +649,11 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
if self._use_timer:
|
if self._use_timer:
|
||||||
# if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
|
# if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
|
||||||
# otherwise business as usual
|
# otherwise business as usual
|
||||||
if self._is_error_check_scheduled:
|
if self._machine_error_checker is None:
|
||||||
|
self._change_timer.stop()
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._machine_error_checker.needToWaitForResult:
|
||||||
self._change_timer.stop()
|
self._change_timer.stop()
|
||||||
else:
|
else:
|
||||||
self._change_timer.start()
|
self._change_timer.start()
|
||||||
|
@ -707,7 +739,11 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
# There is data and we're not slicing at the moment
|
# There is data and we're not slicing at the moment
|
||||||
# if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment.
|
# if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment.
|
||||||
# TODO: what build plate I am slicing
|
# TODO: what build plate I am slicing
|
||||||
if active_build_plate in self._stored_optimized_layer_data and not self._slicing and not self._process_layers_job:
|
if (active_build_plate in self._stored_optimized_layer_data and
|
||||||
|
not self._slicing and
|
||||||
|
not self._process_layers_job and
|
||||||
|
active_build_plate not in self._build_plates_to_be_sliced):
|
||||||
|
|
||||||
self._startProcessSlicedLayersJob(active_build_plate)
|
self._startProcessSlicedLayersJob(active_build_plate)
|
||||||
else:
|
else:
|
||||||
self._layer_view_active = False
|
self._layer_view_active = False
|
||||||
|
@ -773,3 +809,9 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
def tickle(self):
|
def tickle(self):
|
||||||
if self._use_timer:
|
if self._use_timer:
|
||||||
self._change_timer.start()
|
self._change_timer.start()
|
||||||
|
|
||||||
|
def _extruderChanged(self):
|
||||||
|
for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
|
||||||
|
if build_plate_number not in self._build_plates_to_be_sliced:
|
||||||
|
self._build_plates_to_be_sliced.append(build_plate_number)
|
||||||
|
self._invokeSlice()
|
||||||
|
|
|
@ -81,7 +81,8 @@ class ProcessSlicedLayersJob(Job):
|
||||||
|
|
||||||
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
|
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
|
||||||
|
|
||||||
new_node = CuraSceneNode()
|
# The no_setting_override is here because adding the SettingOverrideDecorator will trigger a reslice
|
||||||
|
new_node = CuraSceneNode(no_setting_override = True)
|
||||||
new_node.addDecorator(BuildPlateDecorator(self._build_plate_number))
|
new_node.addDecorator(BuildPlateDecorator(self._build_plate_number))
|
||||||
|
|
||||||
# Force garbage collection.
|
# Force garbage collection.
|
||||||
|
|
|
@ -129,21 +129,19 @@ class StartSliceJob(Job):
|
||||||
self.setResult(StartJobResult.MaterialIncompatible)
|
self.setResult(StartJobResult.MaterialIncompatible)
|
||||||
return
|
return
|
||||||
|
|
||||||
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
|
for position, extruder_stack in stack.extruders.items():
|
||||||
material = extruder_stack.findContainer({"type": "material"})
|
material = extruder_stack.findContainer({"type": "material"})
|
||||||
|
if not extruder_stack.isEnabled:
|
||||||
|
continue
|
||||||
if material:
|
if material:
|
||||||
if material.getMetaDataEntry("compatible") == False:
|
if material.getMetaDataEntry("compatible") == False:
|
||||||
self.setResult(StartJobResult.MaterialIncompatible)
|
self.setResult(StartJobResult.MaterialIncompatible)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Validate settings per selectable model
|
|
||||||
if Application.getInstance().getObjectsModel().stacksHaveErrors():
|
|
||||||
self.setResult(StartJobResult.ObjectSettingError)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Don't slice if there is a per object setting with an error value.
|
# Don't slice if there is a per object setting with an error value.
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
if node.isSelectable():
|
if not isinstance(node, CuraSceneNode) or not node.isSelectable():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self._checkStackForErrors(node.callDecoration("getStack")):
|
if self._checkStackForErrors(node.callDecoration("getStack")):
|
||||||
|
@ -193,8 +191,12 @@ class StartSliceJob(Job):
|
||||||
if per_object_stack:
|
if per_object_stack:
|
||||||
is_non_printing_mesh = any(per_object_stack.getProperty(key, "value") for key in NON_PRINTING_MESH_SETTINGS)
|
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:
|
# Find a reason not to add the node
|
||||||
if not getattr(node, "_outside_buildarea", False) or is_non_printing_mesh:
|
if node.callDecoration("getBuildPlateNumber") != self._build_plate_number:
|
||||||
|
continue
|
||||||
|
if getattr(node, "_outside_buildarea", False) and not is_non_printing_mesh:
|
||||||
|
continue
|
||||||
|
|
||||||
temp_list.append(node)
|
temp_list.append(node)
|
||||||
if not is_non_printing_mesh:
|
if not is_non_printing_mesh:
|
||||||
has_printing_mesh = True
|
has_printing_mesh = True
|
||||||
|
@ -209,10 +211,22 @@ class StartSliceJob(Job):
|
||||||
if temp_list:
|
if temp_list:
|
||||||
object_groups.append(temp_list)
|
object_groups.append(temp_list)
|
||||||
|
|
||||||
|
extruders_enabled = {position: stack.isEnabled for position, stack in Application.getInstance().getGlobalContainerStack().extruders.items()}
|
||||||
|
filtered_object_groups = []
|
||||||
|
for group in object_groups:
|
||||||
|
stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
skip_group = False
|
||||||
|
for node in group:
|
||||||
|
if not extruders_enabled[node.callDecoration("getActiveExtruderPosition")]:
|
||||||
|
skip_group = True
|
||||||
|
break
|
||||||
|
if not skip_group:
|
||||||
|
filtered_object_groups.append(group)
|
||||||
|
|
||||||
# There are cases when there is nothing to slice. This can happen due to one at a time slicing not being
|
# There are cases when there is nothing to slice. This can happen due to one at a time slicing not being
|
||||||
# able to find a possible sequence or because there are no objects on the build plate (or they are outside
|
# able to find a possible sequence or because there are no objects on the build plate (or they are outside
|
||||||
# the build volume)
|
# the build volume)
|
||||||
if not object_groups:
|
if not filtered_object_groups:
|
||||||
self.setResult(StartJobResult.NothingToSlice)
|
self.setResult(StartJobResult.NothingToSlice)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -223,9 +237,9 @@ class StartSliceJob(Job):
|
||||||
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
|
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
|
||||||
self._buildExtruderMessage(extruder_stack)
|
self._buildExtruderMessage(extruder_stack)
|
||||||
|
|
||||||
for group in object_groups:
|
for group in filtered_object_groups:
|
||||||
group_message = self._slice_message.addRepeatedMessage("object_lists")
|
group_message = self._slice_message.addRepeatedMessage("object_lists")
|
||||||
if group[0].getParent().callDecoration("isGroup"):
|
if group[0].getParent() is not None and group[0].getParent().callDecoration("isGroup"):
|
||||||
self._handlePerObjectSettings(group[0].getParent(), group_message)
|
self._handlePerObjectSettings(group[0].getParent(), group_message)
|
||||||
for object in group:
|
for object in group:
|
||||||
mesh_data = object.getMeshData()
|
mesh_data = object.getMeshData()
|
||||||
|
@ -273,9 +287,15 @@ class StartSliceJob(Job):
|
||||||
# \return A dictionary of replacement tokens to the values they should be
|
# \return A dictionary of replacement tokens to the values they should be
|
||||||
# replaced with.
|
# replaced with.
|
||||||
def _buildReplacementTokens(self, stack) -> dict:
|
def _buildReplacementTokens(self, stack) -> dict:
|
||||||
|
default_extruder_position = int(Application.getInstance().getMachineManager().defaultExtruderPosition)
|
||||||
result = {}
|
result = {}
|
||||||
for key in stack.getAllKeys():
|
for key in stack.getAllKeys():
|
||||||
result[key] = stack.getProperty(key, "value")
|
setting_type = stack.definition.getProperty(key, "type")
|
||||||
|
value = stack.getProperty(key, "value")
|
||||||
|
if setting_type == "extruder" and value == -1:
|
||||||
|
# replace with the default value
|
||||||
|
value = default_extruder_position
|
||||||
|
result[key] = value
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
|
|
||||||
result["print_bed_temperature"] = result["material_bed_temperature"] # Renamed settings.
|
result["print_bed_temperature"] = result["material_bed_temperature"] # Renamed settings.
|
||||||
|
@ -381,11 +401,11 @@ class StartSliceJob(Job):
|
||||||
# limit_to_extruder property.
|
# limit_to_extruder property.
|
||||||
def _buildGlobalInheritsStackMessage(self, stack):
|
def _buildGlobalInheritsStackMessage(self, stack):
|
||||||
for key in stack.getAllKeys():
|
for key in stack.getAllKeys():
|
||||||
extruder = int(round(float(stack.getProperty(key, "limit_to_extruder"))))
|
extruder_position = int(round(float(stack.getProperty(key, "limit_to_extruder"))))
|
||||||
if extruder >= 0: #Set to a specific extruder.
|
if extruder_position >= 0: # Set to a specific extruder.
|
||||||
setting_extruder = self._slice_message.addRepeatedMessage("limit_to_extruder")
|
setting_extruder = self._slice_message.addRepeatedMessage("limit_to_extruder")
|
||||||
setting_extruder.name = key
|
setting_extruder.name = key
|
||||||
setting_extruder.extruder = extruder
|
setting_extruder.extruder = extruder_position
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
|
|
||||||
## Check if a node has per object settings and ensure that they are set correctly in the message
|
## Check if a node has per object settings and ensure that they are set correctly in the message
|
||||||
|
|
|
@ -69,7 +69,7 @@ class FirmwareUpdateCheckerJob(Job):
|
||||||
|
|
||||||
# If we do this in a cool way, the download url should be available in the JSON file
|
# If we do this in a cool way, the download url should be available in the JSON file
|
||||||
if self._set_download_url_callback:
|
if self._set_download_url_callback:
|
||||||
self._set_download_url_callback("https://ultimaker.com/en/resources/20500-upgrade-firmware")
|
self._set_download_url_callback("https://ultimaker.com/en/resources/23129-updating-the-firmware?utm_source=cura&utm_medium=software&utm_campaign=hw-update")
|
||||||
message.actionTriggered.connect(self._callback)
|
message.actionTriggered.connect(self._callback)
|
||||||
message.show()
|
message.show()
|
||||||
|
|
||||||
|
|
41
plugins/GCodeGzWriter/GCodeGzWriter.py
Normal file
41
plugins/GCodeGzWriter/GCodeGzWriter.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import gzip
|
||||||
|
from io import StringIO, BufferedIOBase #To write the g-code to a temporary buffer, and for typing.
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Mesh.MeshWriter import MeshWriter #The class we're extending/implementing.
|
||||||
|
from UM.PluginRegistry import PluginRegistry
|
||||||
|
from UM.Scene.SceneNode import SceneNode #For typing.
|
||||||
|
|
||||||
|
## A file writer that writes gzipped g-code.
|
||||||
|
#
|
||||||
|
# If you're zipping g-code, you might as well use gzip!
|
||||||
|
class GCodeGzWriter(MeshWriter):
|
||||||
|
## Writes the gzipped g-code to a stream.
|
||||||
|
#
|
||||||
|
# Note that even though the function accepts a collection of nodes, the
|
||||||
|
# entire scene is always written to the file since it is not possible to
|
||||||
|
# separate the g-code for just specific nodes.
|
||||||
|
#
|
||||||
|
# \param stream The stream to write the gzipped g-code to.
|
||||||
|
# \param nodes This is ignored.
|
||||||
|
# \param mode Additional information on what type of stream to use. This
|
||||||
|
# must always be binary mode.
|
||||||
|
# \return Whether the write was successful.
|
||||||
|
def write(self, stream: BufferedIOBase, nodes: List[SceneNode], mode = MeshWriter.OutputMode.BinaryMode) -> bool:
|
||||||
|
if mode != MeshWriter.OutputMode.BinaryMode:
|
||||||
|
Logger.log("e", "GCodeGzWriter does not support text mode.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
#Get the g-code from the g-code writer.
|
||||||
|
gcode_textio = StringIO() #We have to convert the g-code into bytes.
|
||||||
|
success = PluginRegistry.getInstance().getPluginObject("GCodeWriter").write(gcode_textio, None)
|
||||||
|
if not success: #Writing the g-code failed. Then I can also not write the gzipped g-code.
|
||||||
|
return False
|
||||||
|
|
||||||
|
result = gzip.compress(gcode_textio.getvalue().encode("utf-8"))
|
||||||
|
stream.write(result)
|
||||||
|
return True
|
22
plugins/GCodeGzWriter/__init__.py
Normal file
22
plugins/GCodeGzWriter/__init__.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from . import GCodeGzWriter
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
def getMetaData():
|
||||||
|
return {
|
||||||
|
"mesh_writer": {
|
||||||
|
"output": [{
|
||||||
|
"extension": "gcode.gz",
|
||||||
|
"description": catalog.i18nc("@item:inlistbox", "Compressed G-code File"),
|
||||||
|
"mime_type": "application/gzip",
|
||||||
|
"mode": GCodeGzWriter.GCodeGzWriter.OutputMode.BinaryMode
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def register(app):
|
||||||
|
return { "mesh_writer": GCodeGzWriter.GCodeGzWriter() }
|
8
plugins/GCodeGzWriter/plugin.json
Normal file
8
plugins/GCodeGzWriter/plugin.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "Compressed G-code Writer",
|
||||||
|
"author": "Ultimaker B.V.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Writes g-code to a compressed archive.",
|
||||||
|
"api": 4,
|
||||||
|
"i18n-catalog": "cura"
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ from UM.Logger import Logger
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
from cura.ProfileReader import ProfileReader
|
from cura.ProfileReader import ProfileReader, NoProfileException
|
||||||
|
|
||||||
## A class that reads profile data from g-code files.
|
## A class that reads profile data from g-code files.
|
||||||
#
|
#
|
||||||
|
@ -66,6 +66,11 @@ class GCodeProfileReader(ProfileReader):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
serialized = unescapeGcodeComment(serialized)
|
serialized = unescapeGcodeComment(serialized)
|
||||||
|
serialized = serialized.strip()
|
||||||
|
|
||||||
|
if not serialized:
|
||||||
|
Logger.log("i", "No custom profile to import from this g-code: %s", file_name)
|
||||||
|
raise NoProfileException()
|
||||||
|
|
||||||
# serialized data can be invalid JSON
|
# serialized data can be invalid JSON
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import re # For escaping characters in the settings.
|
||||||
|
import json
|
||||||
|
import copy
|
||||||
|
|
||||||
from UM.Mesh.MeshWriter import MeshWriter
|
from UM.Mesh.MeshWriter import MeshWriter
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
from UM.Util import parseBool
|
|
||||||
|
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
||||||
|
|
||||||
import re #For escaping characters in the settings.
|
|
||||||
import json
|
|
||||||
import copy
|
|
||||||
|
|
||||||
## Writes g-code to a file.
|
## Writes g-code to a file.
|
||||||
#
|
#
|
||||||
|
@ -45,6 +45,8 @@ class GCodeWriter(MeshWriter):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
self._application = Application.getInstance()
|
||||||
|
|
||||||
## Writes the g-code for the entire scene to a stream.
|
## Writes the g-code for the entire scene to a stream.
|
||||||
#
|
#
|
||||||
# Note that even though the function accepts a collection of nodes, the
|
# Note that even though the function accepts a collection of nodes, the
|
||||||
|
@ -94,7 +96,6 @@ class GCodeWriter(MeshWriter):
|
||||||
|
|
||||||
return flat_container
|
return flat_container
|
||||||
|
|
||||||
|
|
||||||
## Serialises a container stack to prepare it for writing at the end of the
|
## Serialises a container stack to prepare it for writing at the end of the
|
||||||
# g-code.
|
# g-code.
|
||||||
#
|
#
|
||||||
|
@ -104,15 +105,20 @@ class GCodeWriter(MeshWriter):
|
||||||
# \param settings A container stack to serialise.
|
# \param settings A container stack to serialise.
|
||||||
# \return A serialised string of the settings.
|
# \return A serialised string of the settings.
|
||||||
def _serialiseSettings(self, stack):
|
def _serialiseSettings(self, stack):
|
||||||
|
container_registry = self._application.getContainerRegistry()
|
||||||
|
quality_manager = self._application.getQualityManager()
|
||||||
|
|
||||||
prefix = ";SETTING_" + str(GCodeWriter.version) + " " # The prefix to put before each line.
|
prefix = ";SETTING_" + str(GCodeWriter.version) + " " # The prefix to put before each line.
|
||||||
prefix_length = len(prefix)
|
prefix_length = len(prefix)
|
||||||
|
|
||||||
|
quality_type = stack.quality.getMetaDataEntry("quality_type")
|
||||||
container_with_profile = stack.qualityChanges
|
container_with_profile = stack.qualityChanges
|
||||||
if container_with_profile.getId() == "empty_quality_changes":
|
if container_with_profile.getId() == "empty_quality_changes":
|
||||||
Logger.log("e", "No valid quality profile found, not writing settings to g-code!")
|
# If the global quality changes is empty, create a new one
|
||||||
return ""
|
quality_name = container_registry.uniqueName(stack.quality.getName())
|
||||||
|
container_with_profile = quality_manager._createQualityChanges(quality_type, quality_name, stack, None)
|
||||||
|
|
||||||
flat_global_container = self._createFlattenedContainerInstance(stack.getTop(), container_with_profile)
|
flat_global_container = self._createFlattenedContainerInstance(stack.userChanges, container_with_profile)
|
||||||
# If the quality changes is not set, we need to set type manually
|
# If the quality changes is not set, we need to set type manually
|
||||||
if flat_global_container.getMetaDataEntry("type", None) is None:
|
if flat_global_container.getMetaDataEntry("type", None) is None:
|
||||||
flat_global_container.addMetaDataEntry("type", "quality_changes")
|
flat_global_container.addMetaDataEntry("type", "quality_changes")
|
||||||
|
@ -121,41 +127,47 @@ class GCodeWriter(MeshWriter):
|
||||||
if flat_global_container.getMetaDataEntry("quality_type", None) is None:
|
if flat_global_container.getMetaDataEntry("quality_type", None) is None:
|
||||||
flat_global_container.addMetaDataEntry("quality_type", stack.quality.getMetaDataEntry("quality_type", "normal"))
|
flat_global_container.addMetaDataEntry("quality_type", stack.quality.getMetaDataEntry("quality_type", "normal"))
|
||||||
|
|
||||||
# Change the default defintion
|
# Get the machine definition ID for quality profiles
|
||||||
default_machine_definition = "fdmprinter"
|
machine_definition_id_for_quality = getMachineDefinitionIDForQualitySearch(stack.definition)
|
||||||
if parseBool(stack.getMetaDataEntry("has_machine_quality", "False")):
|
flat_global_container.setMetaDataEntry("definition", machine_definition_id_for_quality)
|
||||||
default_machine_definition = stack.getMetaDataEntry("quality_definition")
|
|
||||||
if not default_machine_definition:
|
|
||||||
default_machine_definition = stack.definition.getId()
|
|
||||||
flat_global_container.setMetaDataEntry("definition", default_machine_definition)
|
|
||||||
|
|
||||||
serialized = flat_global_container.serialize()
|
serialized = flat_global_container.serialize()
|
||||||
data = {"global_quality": serialized}
|
data = {"global_quality": serialized}
|
||||||
|
|
||||||
for extruder in sorted(stack.extruders.values(), key = lambda k: k.getMetaDataEntry("position")):
|
all_setting_keys = set(flat_global_container.getAllKeys())
|
||||||
|
for extruder in sorted(stack.extruders.values(), key = lambda k: int(k.getMetaDataEntry("position"))):
|
||||||
extruder_quality = extruder.qualityChanges
|
extruder_quality = extruder.qualityChanges
|
||||||
if extruder_quality.getId() == "empty_quality_changes":
|
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())
|
# Same story, if quality changes is empty, create a new one
|
||||||
continue
|
quality_name = container_registry.uniqueName(stack.quality.getName())
|
||||||
flat_extruder_quality = self._createFlattenedContainerInstance(extruder.getTop(), extruder_quality)
|
extruder_quality = quality_manager._createQualityChanges(quality_type, quality_name, stack, None)
|
||||||
|
|
||||||
|
flat_extruder_quality = self._createFlattenedContainerInstance(extruder.userChanges, extruder_quality)
|
||||||
# If the quality changes is not set, we need to set type manually
|
# If the quality changes is not set, we need to set type manually
|
||||||
if flat_extruder_quality.getMetaDataEntry("type", None) is None:
|
if flat_extruder_quality.getMetaDataEntry("type", None) is None:
|
||||||
flat_extruder_quality.addMetaDataEntry("type", "quality_changes")
|
flat_extruder_quality.addMetaDataEntry("type", "quality_changes")
|
||||||
|
|
||||||
# Ensure that extruder is set. (Can happen if we have empty quality changes).
|
# Ensure that extruder is set. (Can happen if we have empty quality changes).
|
||||||
if flat_extruder_quality.getMetaDataEntry("extruder", None) is None:
|
if flat_extruder_quality.getMetaDataEntry("position", None) is None:
|
||||||
flat_extruder_quality.addMetaDataEntry("extruder", extruder.getBottom().getId())
|
flat_extruder_quality.addMetaDataEntry("position", extruder.getMetaDataEntry("position"))
|
||||||
|
|
||||||
# Ensure that quality_type is set. (Can happen if we have empty quality changes).
|
# Ensure that quality_type is set. (Can happen if we have empty quality changes).
|
||||||
if flat_extruder_quality.getMetaDataEntry("quality_type", None) is None:
|
if flat_extruder_quality.getMetaDataEntry("quality_type", None) is None:
|
||||||
flat_extruder_quality.addMetaDataEntry("quality_type", extruder.quality.getMetaDataEntry("quality_type", "normal"))
|
flat_extruder_quality.addMetaDataEntry("quality_type", extruder.quality.getMetaDataEntry("quality_type", "normal"))
|
||||||
|
|
||||||
# Change the default defintion
|
# Change the default definition
|
||||||
flat_extruder_quality.setMetaDataEntry("definition", default_machine_definition)
|
flat_extruder_quality.setMetaDataEntry("definition", machine_definition_id_for_quality)
|
||||||
|
|
||||||
extruder_serialized = flat_extruder_quality.serialize()
|
extruder_serialized = flat_extruder_quality.serialize()
|
||||||
data.setdefault("extruder_quality", []).append(extruder_serialized)
|
data.setdefault("extruder_quality", []).append(extruder_serialized)
|
||||||
|
|
||||||
|
all_setting_keys.update(set(flat_extruder_quality.getAllKeys()))
|
||||||
|
|
||||||
|
# Check if there is any profiles
|
||||||
|
if not all_setting_keys:
|
||||||
|
Logger.log("i", "No custom settings found, not writing settings to g-code.")
|
||||||
|
return ""
|
||||||
|
|
||||||
json_string = json.dumps(data)
|
json_string = json.dumps(data)
|
||||||
|
|
||||||
# Escape characters that have a special meaning in g-code comments.
|
# Escape characters that have a special meaning in g-code comments.
|
||||||
|
|
|
@ -2,20 +2,16 @@
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal
|
||||||
|
|
||||||
|
import UM.i18n
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
|
||||||
from cura.MachineAction import MachineAction
|
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
|
||||||
from UM.Logger import Logger
|
|
||||||
|
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.MachineAction import MachineAction
|
||||||
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
||||||
|
|
||||||
import UM.i18n
|
|
||||||
catalog = UM.i18n.i18nCatalog("cura")
|
catalog = UM.i18n.i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,6 +22,8 @@ class MachineSettingsAction(MachineAction):
|
||||||
super().__init__("MachineSettingsAction", catalog.i18nc("@action", "Machine Settings"))
|
super().__init__("MachineSettingsAction", catalog.i18nc("@action", "Machine Settings"))
|
||||||
self._qml_url = "MachineSettingsAction.qml"
|
self._qml_url = "MachineSettingsAction.qml"
|
||||||
|
|
||||||
|
self._application = Application.getInstance()
|
||||||
|
|
||||||
self._global_container_stack = None
|
self._global_container_stack = None
|
||||||
|
|
||||||
from cura.Settings.CuraContainerStack import _ContainerIndexes
|
from cura.Settings.CuraContainerStack import _ContainerIndexes
|
||||||
|
@ -34,38 +32,44 @@ class MachineSettingsAction(MachineAction):
|
||||||
self._container_registry = ContainerRegistry.getInstance()
|
self._container_registry = ContainerRegistry.getInstance()
|
||||||
self._container_registry.containerAdded.connect(self._onContainerAdded)
|
self._container_registry.containerAdded.connect(self._onContainerAdded)
|
||||||
self._container_registry.containerRemoved.connect(self._onContainerRemoved)
|
self._container_registry.containerRemoved.connect(self._onContainerRemoved)
|
||||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
self._application.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||||
|
|
||||||
self._empty_container = self._container_registry.getEmptyInstanceContainer()
|
self._backend = self._application.getBackend()
|
||||||
|
|
||||||
self._backend = Application.getInstance().getBackend()
|
self._empty_definition_container_id_list = []
|
||||||
|
|
||||||
|
def _isEmptyDefinitionChanges(self, container_id: str):
|
||||||
|
if not self._empty_definition_container_id_list:
|
||||||
|
self._empty_definition_container_id_list = [self._application.empty_container.getId(),
|
||||||
|
self._application.empty_definition_changes_container.getId()]
|
||||||
|
return container_id in self._empty_definition_container_id_list
|
||||||
|
|
||||||
def _onContainerAdded(self, container):
|
def _onContainerAdded(self, container):
|
||||||
# Add this action as a supported action to all machine definitions
|
# Add this action as a supported action to all machine definitions
|
||||||
if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine":
|
if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine":
|
||||||
Application.getInstance().getMachineActionManager().addSupportedAction(container.getId(), self.getKey())
|
self._application.getMachineActionManager().addSupportedAction(container.getId(), self.getKey())
|
||||||
|
|
||||||
def _onContainerRemoved(self, container):
|
def _onContainerRemoved(self, container):
|
||||||
# Remove definition_changes containers when a stack is removed
|
# Remove definition_changes containers when a stack is removed
|
||||||
if container.getMetaDataEntry("type") in ["machine", "extruder_train"]:
|
if container.getMetaDataEntry("type") in ["machine", "extruder_train"]:
|
||||||
definition_changes_container = container.definitionChanges
|
definition_changes_id = container.definitionChanges.getId()
|
||||||
if definition_changes_container == self._empty_container:
|
if self._isEmptyDefinitionChanges(definition_changes_id):
|
||||||
return
|
return
|
||||||
|
|
||||||
self._container_registry.removeContainer(definition_changes_container.getId())
|
self._container_registry.removeContainer(definition_changes_id)
|
||||||
|
|
||||||
def _reset(self):
|
def _reset(self):
|
||||||
if not self._global_container_stack:
|
if not self._global_container_stack:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Make sure there is a definition_changes container to store the machine settings
|
# Make sure there is a definition_changes container to store the machine settings
|
||||||
definition_changes_container = self._global_container_stack.definitionChanges
|
definition_changes_id = self._global_container_stack.definitionChanges.getId()
|
||||||
if definition_changes_container == self._empty_container:
|
if self._isEmptyDefinitionChanges(definition_changes_id):
|
||||||
definition_changes_container = CuraStackBuilder.createDefinitionChangesContainer(
|
CuraStackBuilder.createDefinitionChangesContainer(self._global_container_stack,
|
||||||
self._global_container_stack, self._global_container_stack.getName() + "_settings")
|
self._global_container_stack.getName() + "_settings")
|
||||||
|
|
||||||
# Notify the UI in which container to store the machine settings data
|
# Notify the UI in which container to store the machine settings data
|
||||||
from cura.Settings.CuraContainerStack import CuraContainerStack, _ContainerIndexes
|
from cura.Settings.CuraContainerStack import _ContainerIndexes
|
||||||
|
|
||||||
container_index = _ContainerIndexes.DefinitionChanges
|
container_index = _ContainerIndexes.DefinitionChanges
|
||||||
if container_index != self._container_index:
|
if container_index != self._container_index:
|
||||||
|
@ -107,13 +111,13 @@ class MachineSettingsAction(MachineAction):
|
||||||
def setMachineExtruderCount(self, extruder_count):
|
def setMachineExtruderCount(self, extruder_count):
|
||||||
# Note: this method was in this class before, but since it's quite generic and other plugins also need it
|
# 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.
|
# it was moved to the machine manager instead. Now this method just calls the machine manager.
|
||||||
Application.getInstance().getMachineManager().setActiveMachineExtruderCount(extruder_count)
|
self._application.getMachineManager().setActiveMachineExtruderCount(extruder_count)
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def forceUpdate(self):
|
def forceUpdate(self):
|
||||||
# Force rebuilding the build volume by reloading the global container stack.
|
# Force rebuilding the build volume by reloading the global container stack.
|
||||||
# This is a bit of a hack, but it seems quick enough.
|
# This is a bit of a hack, but it seems quick enough.
|
||||||
Application.getInstance().globalContainerStackChanged.emit()
|
self._application.globalContainerStackChanged.emit()
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def updateHasMaterialsMetadata(self):
|
def updateHasMaterialsMetadata(self):
|
||||||
|
@ -126,9 +130,11 @@ class MachineSettingsAction(MachineAction):
|
||||||
# In other words: only continue for the UM2 (extended), but not for the UM2+
|
# In other words: only continue for the UM2 (extended), but not for the UM2+
|
||||||
return
|
return
|
||||||
|
|
||||||
stacks = ExtruderManager.getInstance().getExtruderStacks()
|
machine_manager = self._application.getMachineManager()
|
||||||
|
extruder_positions = list(self._global_container_stack.extruders.keys())
|
||||||
has_materials = self._global_container_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"
|
has_materials = self._global_container_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"
|
||||||
|
|
||||||
|
material_node = None
|
||||||
if has_materials:
|
if has_materials:
|
||||||
if "has_materials" in self._global_container_stack.getMetaData():
|
if "has_materials" in self._global_container_stack.getMetaData():
|
||||||
self._global_container_stack.setMetaDataEntry("has_materials", True)
|
self._global_container_stack.setMetaDataEntry("has_materials", True)
|
||||||
|
@ -136,26 +142,22 @@ class MachineSettingsAction(MachineAction):
|
||||||
self._global_container_stack.addMetaDataEntry("has_materials", True)
|
self._global_container_stack.addMetaDataEntry("has_materials", True)
|
||||||
|
|
||||||
# Set the material container for each extruder to a sane default
|
# Set the material container for each extruder to a sane default
|
||||||
for stack in stacks:
|
material_manager = self._application.getMaterialManager()
|
||||||
material_container = stack.material
|
material_node = material_manager.getDefaultMaterial(self._global_container_stack, None)
|
||||||
if material_container == self._empty_container:
|
|
||||||
machine_approximate_diameter = str(round(self._global_container_stack.getProperty("material_diameter", "value")))
|
|
||||||
search_criteria = { "type": "material", "definition": "fdmprinter", "id": self._global_container_stack.getMetaDataEntry("preferred_material"), "approximate_diameter": machine_approximate_diameter}
|
|
||||||
materials = self._container_registry.findInstanceContainers(**search_criteria)
|
|
||||||
if materials:
|
|
||||||
stack.material = materials[0]
|
|
||||||
else:
|
else:
|
||||||
# The metadata entry is stored in an ini, and ini files are parsed as strings only.
|
# The metadata entry is stored in an ini, and ini files are parsed as strings only.
|
||||||
# Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
|
# Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
|
||||||
if "has_materials" in self._global_container_stack.getMetaData():
|
if "has_materials" in self._global_container_stack.getMetaData():
|
||||||
self._global_container_stack.removeMetaDataEntry("has_materials")
|
self._global_container_stack.removeMetaDataEntry("has_materials")
|
||||||
|
|
||||||
for stack in stacks:
|
# set materials
|
||||||
stack.material = ContainerRegistry.getInstance().getEmptyInstanceContainer()
|
for position in extruder_positions:
|
||||||
|
machine_manager.setMaterial(position, material_node)
|
||||||
|
|
||||||
Application.getInstance().globalContainerStackChanged.emit()
|
self._application.globalContainerStackChanged.emit()
|
||||||
|
|
||||||
@pyqtSlot(int)
|
@pyqtSlot(int)
|
||||||
def updateMaterialForDiameter(self, extruder_position: int):
|
def updateMaterialForDiameter(self, extruder_position: int):
|
||||||
# Updates the material container to a material that matches the material diameter set for the printer
|
# Updates the material container to a material that matches the material diameter set for the printer
|
||||||
Application.getInstance().getExtruderManager().updateMaterialForDiameter(extruder_position)
|
self._application.getExtruderManager().updateMaterialForDiameter(extruder_position)
|
||||||
|
|
|
@ -244,6 +244,7 @@ Cura.MachineAction
|
||||||
height: childrenRect.height
|
height: childrenRect.height
|
||||||
width: childrenRect.width
|
width: childrenRect.width
|
||||||
text: machineExtruderCountProvider.properties.description
|
text: machineExtruderCountProvider.properties.description
|
||||||
|
visible: extruderCountModel.count >= 2
|
||||||
|
|
||||||
Row
|
Row
|
||||||
{
|
{
|
||||||
|
@ -381,6 +382,11 @@ Cura.MachineAction
|
||||||
property string settingKey: "machine_nozzle_size"
|
property string settingKey: "machine_nozzle_size"
|
||||||
property string label: catalog.i18nc("@label", "Nozzle size")
|
property string label: catalog.i18nc("@label", "Nozzle size")
|
||||||
property string unit: catalog.i18nc("@label", "mm")
|
property string unit: catalog.i18nc("@label", "mm")
|
||||||
|
function afterOnEditingFinished()
|
||||||
|
{
|
||||||
|
// Somehow the machine_nozzle_size dependent settings are not updated otherwise
|
||||||
|
Cura.MachineManager.forceUpdateAllSettings()
|
||||||
|
}
|
||||||
property bool isExtruderSetting: true
|
property bool isExtruderSetting: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,14 +22,7 @@ class MonitorStage(CuraStage):
|
||||||
|
|
||||||
def _setActivePrintJob(self, print_job):
|
def _setActivePrintJob(self, print_job):
|
||||||
if self._active_print_job != 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
|
self._active_print_job = print_job
|
||||||
if self._active_print_job:
|
|
||||||
self._active_print_job.stateChanged.connect(self._updateIconSource)
|
|
||||||
|
|
||||||
# Ensure that the right icon source is returned.
|
|
||||||
self._updateIconSource()
|
|
||||||
|
|
||||||
def _setActivePrinter(self, printer):
|
def _setActivePrinter(self, printer):
|
||||||
if self._active_printer != printer:
|
if self._active_printer != printer:
|
||||||
|
@ -43,9 +36,6 @@ class MonitorStage(CuraStage):
|
||||||
else:
|
else:
|
||||||
self._setActivePrintJob(None)
|
self._setActivePrintJob(None)
|
||||||
|
|
||||||
# Ensure that the right icon source is returned.
|
|
||||||
self._updateIconSource()
|
|
||||||
|
|
||||||
def _onActivePrintJobChanged(self):
|
def _onActivePrintJobChanged(self):
|
||||||
self._setActivePrintJob(self._active_printer.activePrintJob)
|
self._setActivePrintJob(self._active_printer.activePrintJob)
|
||||||
|
|
||||||
|
@ -58,22 +48,17 @@ class MonitorStage(CuraStage):
|
||||||
new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
|
new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
|
||||||
if new_output_device != self._printer_output_device:
|
if new_output_device != self._printer_output_device:
|
||||||
if self._printer_output_device:
|
if self._printer_output_device:
|
||||||
self._printer_output_device.acceptsCommandsChanged.disconnect(self._updateIconSource)
|
try:
|
||||||
self._printer_output_device.connectionStateChanged.disconnect(self._updateIconSource)
|
|
||||||
self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged)
|
self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged)
|
||||||
|
except TypeError:
|
||||||
|
# Ignore stupid "Not connected" errors.
|
||||||
|
pass
|
||||||
|
|
||||||
self._printer_output_device = new_output_device
|
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.printersChanged.connect(self._onActivePrinterChanged)
|
||||||
self._printer_output_device.connectionStateChanged.connect(self._updateIconSource)
|
|
||||||
self._setActivePrinter(self._printer_output_device.activePrinter)
|
self._setActivePrinter(self._printer_output_device.activePrinter)
|
||||||
|
|
||||||
# Force an update of the icon source
|
|
||||||
self._updateIconSource()
|
|
||||||
except IndexError:
|
except IndexError:
|
||||||
#If index error occurs, then the icon on monitor button also should be updated
|
|
||||||
self._updateIconSource()
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _onEngineCreated(self):
|
def _onEngineCreated(self):
|
||||||
|
@ -82,7 +67,6 @@ class MonitorStage(CuraStage):
|
||||||
self._onOutputDevicesChanged()
|
self._onOutputDevicesChanged()
|
||||||
self._updateMainOverlay()
|
self._updateMainOverlay()
|
||||||
self._updateSidebar()
|
self._updateSidebar()
|
||||||
self._updateIconSource()
|
|
||||||
|
|
||||||
def _updateMainOverlay(self):
|
def _updateMainOverlay(self):
|
||||||
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"), "MonitorMainView.qml")
|
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"), "MonitorMainView.qml")
|
||||||
|
@ -92,46 +76,3 @@ class MonitorStage(CuraStage):
|
||||||
# TODO: currently the sidebar component for prepare and monitor stages is the same, this will change with the printer output device refactor!
|
# 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")
|
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "Sidebar.qml")
|
||||||
self.addDisplayComponent("sidebar", sidebar_component_path)
|
self.addDisplayComponent("sidebar", sidebar_component_path)
|
||||||
|
|
||||||
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):
|
|
||||||
# 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 not output_device.acceptsCommands:
|
|
||||||
return "tab_status_unknown"
|
|
||||||
|
|
||||||
if output_device.activePrinter is None:
|
|
||||||
return "tab_status_connected"
|
|
||||||
|
|
||||||
# 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.activePrinter.activePrintJob.state == "error":
|
|
||||||
return "tab_status_stopped"
|
|
||||||
|
|
||||||
return "tab_status_unknown"
|
|
||||||
|
|
|
@ -24,21 +24,8 @@ UM.TooltipArea
|
||||||
checked: addedSettingsModel.getVisible(model.key)
|
checked: addedSettingsModel.getVisible(model.key)
|
||||||
|
|
||||||
onClicked:
|
onClicked:
|
||||||
{
|
|
||||||
// Important first set visible and then subscribe
|
|
||||||
// otherwise the setting is not yet in list
|
|
||||||
// For unsubscribe is important first remove the subscription and then
|
|
||||||
// set as invisible
|
|
||||||
if(checked)
|
|
||||||
{
|
{
|
||||||
addedSettingsModel.setVisible(model.key, checked);
|
addedSettingsModel.setVisible(model.key, checked);
|
||||||
UM.ActiveTool.triggerActionWithData("subscribeForSettingValidation", model.key)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UM.ActiveTool.triggerActionWithData("unsubscribeForSettingValidation", model.key)
|
|
||||||
addedSettingsModel.setVisible(model.key, checked);
|
|
||||||
}
|
|
||||||
UM.ActiveTool.forceUpdate();
|
UM.ActiveTool.forceUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -163,7 +163,16 @@ Item {
|
||||||
id: addedSettingsModel;
|
id: addedSettingsModel;
|
||||||
containerId: Cura.MachineManager.activeDefinitionId
|
containerId: Cura.MachineManager.activeDefinitionId
|
||||||
expanded: [ "*" ]
|
expanded: [ "*" ]
|
||||||
exclude: {
|
filter:
|
||||||
|
{
|
||||||
|
if (printSequencePropertyProvider.properties.value == "one_at_a_time")
|
||||||
|
{
|
||||||
|
return {"settable_per_meshgroup": true};
|
||||||
|
}
|
||||||
|
return {"settable_per_mesh": true};
|
||||||
|
}
|
||||||
|
exclude:
|
||||||
|
{
|
||||||
var excluded_settings = [ "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ];
|
var excluded_settings = [ "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ];
|
||||||
|
|
||||||
if(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type == "support_mesh")
|
if(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type == "support_mesh")
|
||||||
|
@ -240,10 +249,7 @@ Item {
|
||||||
width: Math.round(UM.Theme.getSize("setting").height / 2)
|
width: Math.round(UM.Theme.getSize("setting").height / 2)
|
||||||
height: UM.Theme.getSize("setting").height
|
height: UM.Theme.getSize("setting").height
|
||||||
|
|
||||||
onClicked: {
|
onClicked: addedSettingsModel.setVisible(model.key, false)
|
||||||
addedSettingsModel.setVisible(model.key, false)
|
|
||||||
UM.ActiveTool.triggerActionWithData("unsubscribeForSettingValidation", model.key)
|
|
||||||
}
|
|
||||||
|
|
||||||
style: ButtonStyle
|
style: ButtonStyle
|
||||||
{
|
{
|
||||||
|
@ -378,7 +384,6 @@ Item {
|
||||||
title: catalog.i18nc("@title:window", "Select Settings to Customize for this model")
|
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
|
property var additional_excluded_settings
|
||||||
|
|
||||||
onVisibilityChanged:
|
onVisibilityChanged:
|
||||||
|
@ -389,11 +394,33 @@ Item {
|
||||||
// Set skip setting, it will prevent from resetting selected mesh_type
|
// Set skip setting, it will prevent from resetting selected mesh_type
|
||||||
contents.model.visibilityHandler.addSkipResetSetting(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type)
|
contents.model.visibilityHandler.addSkipResetSetting(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type)
|
||||||
listview.model.forceUpdate()
|
listview.model.forceUpdate()
|
||||||
|
|
||||||
|
updateFilter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateFilter()
|
||||||
|
{
|
||||||
|
var new_filter = {};
|
||||||
|
if (printSequencePropertyProvider.properties.value == "one_at_a_time")
|
||||||
|
{
|
||||||
|
new_filter["settable_per_meshgroup"] = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
new_filter["settable_per_mesh"] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(filterInput.text != "")
|
||||||
|
{
|
||||||
|
new_filter["i18n_label"] = "*" + filterInput.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
listview.model.filter = new_filter;
|
||||||
|
}
|
||||||
|
|
||||||
TextField {
|
TextField {
|
||||||
id: filter
|
id: filterInput
|
||||||
|
|
||||||
anchors {
|
anchors {
|
||||||
top: parent.top
|
top: parent.top
|
||||||
|
@ -404,17 +431,7 @@ Item {
|
||||||
|
|
||||||
placeholderText: catalog.i18nc("@label:textbox", "Filter...");
|
placeholderText: catalog.i18nc("@label:textbox", "Filter...");
|
||||||
|
|
||||||
onTextChanged:
|
onTextChanged: settingPickDialog.updateFilter()
|
||||||
{
|
|
||||||
if(text != "")
|
|
||||||
{
|
|
||||||
listview.model.filter = {"settable_per_mesh": true, "i18n_label": "*" + text}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
listview.model.filter = {"settable_per_mesh": true}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckBox
|
CheckBox
|
||||||
|
@ -440,7 +457,7 @@ Item {
|
||||||
|
|
||||||
anchors
|
anchors
|
||||||
{
|
{
|
||||||
top: filter.bottom;
|
top: filterInput.bottom;
|
||||||
left: parent.left;
|
left: parent.left;
|
||||||
right: parent.right;
|
right: parent.right;
|
||||||
bottom: parent.bottom;
|
bottom: parent.bottom;
|
||||||
|
@ -452,10 +469,6 @@ Item {
|
||||||
{
|
{
|
||||||
id: definitionsModel;
|
id: definitionsModel;
|
||||||
containerId: Cura.MachineManager.activeDefinitionId
|
containerId: Cura.MachineManager.activeDefinitionId
|
||||||
filter:
|
|
||||||
{
|
|
||||||
"settable_per_mesh": true
|
|
||||||
}
|
|
||||||
visibilityHandler: UM.SettingPreferenceVisibilityHandler {}
|
visibilityHandler: UM.SettingPreferenceVisibilityHandler {}
|
||||||
expanded: [ "*" ]
|
expanded: [ "*" ]
|
||||||
exclude:
|
exclude:
|
||||||
|
@ -487,6 +500,7 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Component.onCompleted: settingPickDialog.updateFilter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -510,6 +524,16 @@ Item {
|
||||||
storeIndex: 0
|
storeIndex: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UM.SettingPropertyProvider
|
||||||
|
{
|
||||||
|
id: printSequencePropertyProvider
|
||||||
|
|
||||||
|
containerStackId: Cura.MachineManager.activeMachineId
|
||||||
|
key: "print_sequence"
|
||||||
|
watchedProperties: [ "value" ]
|
||||||
|
storeIndex: 0
|
||||||
|
}
|
||||||
|
|
||||||
SystemPalette { id: palette; }
|
SystemPalette { id: palette; }
|
||||||
|
|
||||||
Component
|
Component
|
||||||
|
|
|
@ -10,10 +10,7 @@ from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
from UM.Settings.SettingInstance import SettingInstance
|
from UM.Settings.SettingInstance import SettingInstance
|
||||||
from UM.Event import Event
|
from UM.Event import Event
|
||||||
from UM.Settings.Validator import ValidatorState
|
|
||||||
from UM.Logger import Logger
|
|
||||||
|
|
||||||
from PyQt5.QtCore import QTimer
|
|
||||||
|
|
||||||
## This tool allows the user to add & change settings per node in the scene.
|
## This tool allows the user to add & change settings per node in the scene.
|
||||||
# The settings per object are kept in a ContainerStack, which is linked to a node by decorator.
|
# The settings per object are kept in a ContainerStack, which is linked to a node by decorator.
|
||||||
|
@ -37,12 +34,6 @@ class PerObjectSettingsTool(Tool):
|
||||||
self._onGlobalContainerChanged()
|
self._onGlobalContainerChanged()
|
||||||
Selection.selectionChanged.connect(self._updateEnabled)
|
Selection.selectionChanged.connect(self._updateEnabled)
|
||||||
|
|
||||||
self._scene = Application.getInstance().getController().getScene()
|
|
||||||
|
|
||||||
self._error_check_timer = QTimer()
|
|
||||||
self._error_check_timer.setInterval(250)
|
|
||||||
self._error_check_timer.setSingleShot(True)
|
|
||||||
self._error_check_timer.timeout.connect(self._updateStacksHaveErrors)
|
|
||||||
|
|
||||||
def event(self, event):
|
def event(self, event):
|
||||||
super().event(event)
|
super().event(event)
|
||||||
|
@ -151,65 +142,3 @@ class PerObjectSettingsTool(Tool):
|
||||||
else:
|
else:
|
||||||
self._single_model_selected = True
|
self._single_model_selected = True
|
||||||
Application.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, self._advanced_mode and self._single_model_selected)
|
Application.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, self._advanced_mode and self._single_model_selected)
|
||||||
|
|
||||||
|
|
||||||
def _onPropertyChanged(self, key: str, property_name: str) -> None:
|
|
||||||
if property_name == "validationState":
|
|
||||||
# self._error_check_timer.start()
|
|
||||||
return
|
|
||||||
|
|
||||||
def _updateStacksHaveErrors(self) -> None:
|
|
||||||
return
|
|
||||||
# self._checkStacksHaveErrors()
|
|
||||||
|
|
||||||
|
|
||||||
def _checkStacksHaveErrors(self):
|
|
||||||
|
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
|
||||||
|
|
||||||
# valdiate only objects which can be selected because the settings per object
|
|
||||||
# can be applied only for them
|
|
||||||
if not node.isSelectable():
|
|
||||||
continue
|
|
||||||
|
|
||||||
hasErrors = self._checkStackForErrors(node.callDecoration("getStack"))
|
|
||||||
Application.getInstance().getObjectsModel().setStacksHaveErrors(hasErrors)
|
|
||||||
|
|
||||||
#If any of models has an error then no reason check next objects on the build plate
|
|
||||||
if hasErrors:
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def _checkStackForErrors(self, stack):
|
|
||||||
print("checking for errors")
|
|
||||||
if stack is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
for key in stack.getAllKeys():
|
|
||||||
validation_state = stack.getProperty(key, "validationState")
|
|
||||||
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
|
|
||||||
Logger.log("w", "Setting Per Object %s is not valid.", key)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def subscribeForSettingValidation(self, setting_name):
|
|
||||||
selected_object = Selection.getSelectedObject(0)
|
|
||||||
stack = selected_object.callDecoration("getStack") # Don't try to get the active extruder since it may be None anyway.
|
|
||||||
if not stack:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
settings = stack.getTop()
|
|
||||||
setting_instance = settings.getInstance(setting_name)
|
|
||||||
if setting_instance:
|
|
||||||
setting_instance.propertyChanged.connect(self._onPropertyChanged)
|
|
||||||
|
|
||||||
def unsubscribeForSettingValidation(self, setting_name):
|
|
||||||
selected_object = Selection.getSelectedObject(0)
|
|
||||||
stack = selected_object.callDecoration("getStack") # Don't try to get the active extruder since it may be None anyway.
|
|
||||||
if not stack:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
settings = stack.getTop()
|
|
||||||
setting_instance = settings.getInstance(setting_name)
|
|
||||||
if setting_instance:
|
|
||||||
setting_instance.propertyChanged.disconnect(self._onPropertyChanged)
|
|
||||||
|
|
|
@ -173,7 +173,10 @@ class PostProcessingPlugin(QObject, Extension):
|
||||||
Logger.log("d", "Creating post processing plugin view.")
|
Logger.log("d", "Creating post processing plugin view.")
|
||||||
|
|
||||||
## Load all scripts in the scripts folders
|
## Load all scripts in the scripts folders
|
||||||
for root in [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Preferences)]:
|
# The PostProcessingPlugin path is for built-in scripts.
|
||||||
|
# The Resources path is where the user should store custom scripts.
|
||||||
|
# The Preferences path is legacy, where the user may previously have stored scripts.
|
||||||
|
for root in [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Resources), Resources.getStoragePath(Resources.Preferences)]:
|
||||||
path = os.path.join(root, "scripts")
|
path = os.path.join(root, "scripts")
|
||||||
if not os.path.isdir(path):
|
if not os.path.isdir(path):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -2,17 +2,15 @@
|
||||||
# under the terms of the AGPLv3 or higher
|
# under the terms of the AGPLv3 or higher
|
||||||
|
|
||||||
from ..Script import Script
|
from ..Script import Script
|
||||||
#from UM.Logger import Logger
|
|
||||||
# from cura.Settings.ExtruderManager import ExtruderManager
|
|
||||||
|
|
||||||
class ColorChange(Script):
|
class FilamentChange(Script):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def getSettingDataString(self):
|
def getSettingDataString(self):
|
||||||
return """{
|
return """{
|
||||||
"name":"Color Change",
|
"name":"Filament Change",
|
||||||
"key": "ColorChange",
|
"key": "FilamentChange",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"settings":
|
"settings":
|
||||||
|
@ -29,18 +27,18 @@ class ColorChange(Script):
|
||||||
"initial_retract":
|
"initial_retract":
|
||||||
{
|
{
|
||||||
"label": "Initial Retraction",
|
"label": "Initial Retraction",
|
||||||
"description": "Initial filament retraction distance",
|
"description": "Initial filament retraction distance. The filament will be retracted with this amount before moving the nozzle away from the ongoing print.",
|
||||||
"unit": "mm",
|
"unit": "mm",
|
||||||
"type": "float",
|
"type": "float",
|
||||||
"default_value": 300.0
|
"default_value": 30.0
|
||||||
},
|
},
|
||||||
"later_retract":
|
"later_retract":
|
||||||
{
|
{
|
||||||
"label": "Later Retraction Distance",
|
"label": "Later Retraction Distance",
|
||||||
"description": "Later filament retraction distance for removal",
|
"description": "Later filament retraction distance for removal. The filament will be retracted all the way out of the printer so that you can change the filament.",
|
||||||
"unit": "mm",
|
"unit": "mm",
|
||||||
"type": "float",
|
"type": "float",
|
||||||
"default_value": 30.0
|
"default_value": 300.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}"""
|
}"""
|
||||||
|
@ -60,9 +58,9 @@ class ColorChange(Script):
|
||||||
if later_retract is not None and later_retract > 0.:
|
if later_retract is not None and later_retract > 0.:
|
||||||
color_change = color_change + (" L%.2f" % later_retract)
|
color_change = color_change + (" L%.2f" % later_retract)
|
||||||
|
|
||||||
color_change = color_change + " ; Generated by ColorChange plugin"
|
color_change = color_change + " ; Generated by FilamentChange plugin"
|
||||||
|
|
||||||
layer_targets = layer_nums.split(',')
|
layer_targets = layer_nums.split(",")
|
||||||
if len(layer_targets) > 0:
|
if len(layer_targets) > 0:
|
||||||
for layer_num in layer_targets:
|
for layer_num in layer_targets:
|
||||||
layer_num = int(layer_num.strip())
|
layer_num = int(layer_num.strip())
|
|
@ -12,6 +12,7 @@ import numpy as np
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
import re
|
import re
|
||||||
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
|
||||||
def _getValue(line, key, default=None):
|
def _getValue(line, key, default=None):
|
||||||
"""
|
"""
|
||||||
|
@ -90,9 +91,9 @@ class Stretcher():
|
||||||
"""
|
"""
|
||||||
Computes the new X and Y coordinates of all g-code steps
|
Computes the new X and Y coordinates of all g-code steps
|
||||||
"""
|
"""
|
||||||
Logger.log("d", "Post stretch with line width = " + str(self.line_width)
|
Logger.log("d", "Post stretch with line width " + str(self.line_width)
|
||||||
+ "mm wide circle stretch = " + str(self.wc_stretch)+ "mm"
|
+ "mm wide circle stretch " + str(self.wc_stretch)+ "mm"
|
||||||
+ "and push wall stretch = " + str(self.pw_stretch) + "mm")
|
+ " and push wall stretch " + str(self.pw_stretch) + "mm")
|
||||||
retdata = []
|
retdata = []
|
||||||
layer_steps = []
|
layer_steps = []
|
||||||
current = GCodeStep(0)
|
current = GCodeStep(0)
|
||||||
|
@ -282,7 +283,7 @@ class Stretcher():
|
||||||
dmin_tri is the minimum distance between two consecutive points
|
dmin_tri is the minimum distance between two consecutive points
|
||||||
of an acceptable triangle
|
of an acceptable triangle
|
||||||
"""
|
"""
|
||||||
dmin_tri = self.line_width / 2.0
|
dmin_tri = 0.5
|
||||||
iextra_base = np.floor_divide(len(orig_seq), 3) # Nb of extra points
|
iextra_base = np.floor_divide(len(orig_seq), 3) # Nb of extra points
|
||||||
ibeg = 0 # Index of first point of the triangle
|
ibeg = 0 # Index of first point of the triangle
|
||||||
iend = 0 # Index of the third point of the triangle
|
iend = 0 # Index of the third point of the triangle
|
||||||
|
@ -325,9 +326,10 @@ class Stretcher():
|
||||||
relpos = 0.5 # To avoid division by zero or precision loss
|
relpos = 0.5 # To avoid division by zero or precision loss
|
||||||
projection = (pos_before[ibeg] + relpos * (pos_after[iend] - pos_before[ibeg]))
|
projection = (pos_before[ibeg] + relpos * (pos_after[iend] - pos_before[ibeg]))
|
||||||
dist_from_proj = np.sqrt(((projection - step) ** 2).sum(0))
|
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
|
if dist_from_proj > 0.0003: # Move central point only if points are not aligned
|
||||||
modif_seq[i] = (step - (self.wc_stretch / dist_from_proj)
|
modif_seq[i] = (step - (self.wc_stretch / dist_from_proj)
|
||||||
* (projection - step))
|
* (projection - step))
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def wideTurn(self, orig_seq, modif_seq):
|
def wideTurn(self, orig_seq, modif_seq):
|
||||||
|
@ -411,8 +413,6 @@ class Stretcher():
|
||||||
modif_seq[ibeg] = modif_seq[ibeg] + xperp * self.pw_stretch
|
modif_seq[ibeg] = modif_seq[ibeg] + xperp * self.pw_stretch
|
||||||
elif not materialleft and materialright:
|
elif not materialleft and materialright:
|
||||||
modif_seq[ibeg] = modif_seq[ibeg] - xperp * self.pw_stretch
|
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
|
# Setup part of the stretch plugin
|
||||||
class Stretch(Script):
|
class Stretch(Script):
|
||||||
|
@ -437,7 +437,7 @@ class Stretch(Script):
|
||||||
"description": "Distance by which the points are moved by the correction effect in corners. The higher this value, the higher the effect",
|
"description": "Distance by which the points are moved by the correction effect in corners. The higher this value, the higher the effect",
|
||||||
"unit": "mm",
|
"unit": "mm",
|
||||||
"type": "float",
|
"type": "float",
|
||||||
"default_value": 0.08,
|
"default_value": 0.1,
|
||||||
"minimum_value": 0,
|
"minimum_value": 0,
|
||||||
"minimum_value_warning": 0,
|
"minimum_value_warning": 0,
|
||||||
"maximum_value_warning": 0.2
|
"maximum_value_warning": 0.2
|
||||||
|
@ -448,7 +448,7 @@ class Stretch(Script):
|
||||||
"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",
|
"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",
|
"unit": "mm",
|
||||||
"type": "float",
|
"type": "float",
|
||||||
"default_value": 0.08,
|
"default_value": 0.1,
|
||||||
"minimum_value": 0,
|
"minimum_value": 0,
|
||||||
"minimum_value_warning": 0,
|
"minimum_value_warning": 0,
|
||||||
"maximum_value_warning": 0.2
|
"maximum_value_warning": 0.2
|
||||||
|
@ -463,7 +463,7 @@ class Stretch(Script):
|
||||||
the returned string is the list of modified g-code instructions
|
the returned string is the list of modified g-code instructions
|
||||||
"""
|
"""
|
||||||
stretcher = Stretcher(
|
stretcher = Stretcher(
|
||||||
Application.getInstance().getGlobalContainerStack().getProperty("line_width", "value")
|
ExtruderManager.getInstance().getActiveExtruderStack().getProperty("machine_nozzle_size", "value")
|
||||||
, self.getSettingValueByKey("wc_stretch"), self.getSettingValueByKey("pw_stretch"))
|
, self.getSettingValueByKey("wc_stretch"), self.getSettingValueByKey("pw_stretch"))
|
||||||
return stretcher.execute(data)
|
return stretcher.execute(data)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
|
@ -7,7 +7,7 @@ from UM.Application import Application
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
from UM.FileHandler.WriteFileJob import WriteFileJob
|
from UM.FileHandler.WriteFileJob import WriteFileJob
|
||||||
from UM.Mesh.MeshWriter import MeshWriter
|
from UM.FileHandler.FileWriter import FileWriter #To check against the write modes (text vs. binary).
|
||||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||||
from UM.OutputDevice.OutputDevice import OutputDevice
|
from UM.OutputDevice.OutputDevice import OutputDevice
|
||||||
from UM.OutputDevice import OutputDeviceError
|
from UM.OutputDevice import OutputDeviceError
|
||||||
|
@ -39,7 +39,7 @@ class RemovableDriveOutputDevice(OutputDevice):
|
||||||
# MIME types available to the currently active machine?
|
# MIME types available to the currently active machine?
|
||||||
#
|
#
|
||||||
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
|
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
|
||||||
filter_by_machine = True # This plugin is indended to be used by machine (regardless of what it was told to do)
|
filter_by_machine = True # This plugin is intended to be used by machine (regardless of what it was told to do)
|
||||||
if self._writing:
|
if self._writing:
|
||||||
raise OutputDeviceError.DeviceBusyError()
|
raise OutputDeviceError.DeviceBusyError()
|
||||||
|
|
||||||
|
@ -56,19 +56,21 @@ class RemovableDriveOutputDevice(OutputDevice):
|
||||||
machine_file_formats = [file_type.strip() for file_type in container.getMetaDataEntry("file_formats").split(";")]
|
machine_file_formats = [file_type.strip() for file_type in container.getMetaDataEntry("file_formats").split(";")]
|
||||||
|
|
||||||
# Take the intersection between file_formats and machine_file_formats.
|
# Take the intersection between file_formats and machine_file_formats.
|
||||||
file_formats = list(filter(lambda file_format: file_format["mime_type"] in machine_file_formats, file_formats))
|
format_by_mimetype = {format["mime_type"]: format for format in file_formats}
|
||||||
|
file_formats = [format_by_mimetype[mimetype] for mimetype in machine_file_formats] #Keep them ordered according to the preference in machine_file_formats.
|
||||||
|
|
||||||
if len(file_formats) == 0:
|
if len(file_formats) == 0:
|
||||||
Logger.log("e", "There are no file formats available to write with!")
|
Logger.log("e", "There are no file formats available to write with!")
|
||||||
raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("There are no file formats available to write with!"))
|
raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("@info:status", "There are no file formats available to write with!"))
|
||||||
|
preferred_format = file_formats[0]
|
||||||
|
|
||||||
# Just take the first file format available.
|
# Just take the first file format available.
|
||||||
if file_handler is not None:
|
if file_handler is not None:
|
||||||
writer = file_handler.getWriterByMimeType(file_formats[0]["mime_type"])
|
writer = file_handler.getWriterByMimeType(preferred_format["mime_type"])
|
||||||
else:
|
else:
|
||||||
writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(file_formats[0]["mime_type"])
|
writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(preferred_format["mime_type"])
|
||||||
|
|
||||||
extension = file_formats[0]["extension"]
|
extension = preferred_format["extension"]
|
||||||
|
|
||||||
if file_name is None:
|
if file_name is None:
|
||||||
file_name = self._automaticFileName(nodes)
|
file_name = self._automaticFileName(nodes)
|
||||||
|
@ -80,8 +82,11 @@ class RemovableDriveOutputDevice(OutputDevice):
|
||||||
try:
|
try:
|
||||||
Logger.log("d", "Writing to %s", file_name)
|
Logger.log("d", "Writing to %s", file_name)
|
||||||
# Using buffering greatly reduces the write time for many lines of gcode
|
# Using buffering greatly reduces the write time for many lines of gcode
|
||||||
|
if preferred_format["mode"] == FileWriter.OutputMode.TextMode:
|
||||||
self._stream = open(file_name, "wt", buffering = 1, encoding = "utf-8")
|
self._stream = open(file_name, "wt", buffering = 1, encoding = "utf-8")
|
||||||
job = WriteFileJob(writer, self._stream, nodes, MeshWriter.OutputMode.TextMode)
|
else: #Binary mode.
|
||||||
|
self._stream = open(file_name, "wb", buffering = 1)
|
||||||
|
job = WriteFileJob(writer, self._stream, nodes, preferred_format["mode"])
|
||||||
job.setFileName(file_name)
|
job.setFileName(file_name)
|
||||||
job.progress.connect(self._onProgress)
|
job.progress.connect(self._onProgress)
|
||||||
job.finished.connect(self._onFinished)
|
job.finished.connect(self._onFinished)
|
||||||
|
|
|
@ -74,7 +74,7 @@ class SimulationView(View):
|
||||||
|
|
||||||
self._global_container_stack = None
|
self._global_container_stack = None
|
||||||
self._proxy = SimulationViewProxy()
|
self._proxy = SimulationViewProxy()
|
||||||
self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged)
|
self._controller.getScene().sceneChanged.connect(self._onSceneChanged)
|
||||||
|
|
||||||
self._resetSettings()
|
self._resetSettings()
|
||||||
self._legend_items = None
|
self._legend_items = None
|
||||||
|
@ -158,6 +158,9 @@ class SimulationView(View):
|
||||||
return self._nozzle_node
|
return self._nozzle_node
|
||||||
|
|
||||||
def _onSceneChanged(self, node):
|
def _onSceneChanged(self, node):
|
||||||
|
if node.getMeshData() is None:
|
||||||
|
self.resetLayerData()
|
||||||
|
else:
|
||||||
self.setActivity(False)
|
self.setActivity(False)
|
||||||
self.calculateMaxLayers()
|
self.calculateMaxLayers()
|
||||||
self.calculateMaxPathsOnLayer(self._current_layer_num)
|
self.calculateMaxPathsOnLayer(self._current_layer_num)
|
||||||
|
|
|
@ -146,7 +146,7 @@ class SliceInfo(Extension):
|
||||||
model_stack = node.callDecoration("getStack")
|
model_stack = node.callDecoration("getStack")
|
||||||
if model_stack:
|
if model_stack:
|
||||||
model_settings["support_enabled"] = model_stack.getProperty("support_enable", "value")
|
model_settings["support_enabled"] = model_stack.getProperty("support_enable", "value")
|
||||||
model_settings["support_extruder_nr"] = int(model_stack.getProperty("support_extruder_nr", "value"))
|
model_settings["support_extruder_nr"] = int(model_stack.getExtruderPositionValueWithDefault("support_extruder_nr"))
|
||||||
|
|
||||||
# Mesh modifiers;
|
# Mesh modifiers;
|
||||||
model_settings["infill_mesh"] = model_stack.getProperty("infill_mesh", "value")
|
model_settings["infill_mesh"] = model_stack.getProperty("infill_mesh", "value")
|
||||||
|
@ -177,7 +177,7 @@ class SliceInfo(Extension):
|
||||||
|
|
||||||
# Support settings
|
# Support settings
|
||||||
print_settings["support_enabled"] = global_container_stack.getProperty("support_enable", "value")
|
print_settings["support_enabled"] = global_container_stack.getProperty("support_enable", "value")
|
||||||
print_settings["support_extruder_nr"] = int(global_container_stack.getProperty("support_extruder_nr", "value"))
|
print_settings["support_extruder_nr"] = int(global_container_stack.getExtruderPositionValueWithDefault("support_extruder_nr"))
|
||||||
|
|
||||||
# Platform adhesion settings
|
# Platform adhesion settings
|
||||||
print_settings["adhesion_type"] = global_container_stack.getProperty("adhesion_type", "value")
|
print_settings["adhesion_type"] = global_container_stack.getProperty("adhesion_type", "value")
|
||||||
|
|
|
@ -62,7 +62,7 @@ class SolidView(View):
|
||||||
|
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if global_container_stack:
|
if global_container_stack:
|
||||||
support_extruder_nr = global_container_stack.getProperty("support_extruder_nr", "value")
|
support_extruder_nr = global_container_stack.getExtruderPositionValueWithDefault("support_extruder_nr")
|
||||||
support_angle_stack = Application.getInstance().getExtruderManager().getExtruderStack(support_extruder_nr)
|
support_angle_stack = Application.getInstance().getExtruderManager().getExtruderStack(support_extruder_nr)
|
||||||
|
|
||||||
if support_angle_stack is not None and Preferences.getInstance().getValue("view/show_overhang"):
|
if support_angle_stack is not None and Preferences.getInstance().getValue("view/show_overhang"):
|
||||||
|
@ -78,22 +78,18 @@ class SolidView(View):
|
||||||
|
|
||||||
for node in DepthFirstIterator(scene.getRoot()):
|
for node in DepthFirstIterator(scene.getRoot()):
|
||||||
if not node.render(renderer):
|
if not node.render(renderer):
|
||||||
if node.getMeshData() and node.isVisible():
|
if node.getMeshData() and node.isVisible() and not node.callDecoration("getLayerData"):
|
||||||
uniforms = {}
|
uniforms = {}
|
||||||
shade_factor = 1.0
|
shade_factor = 1.0
|
||||||
|
|
||||||
per_mesh_stack = node.callDecoration("getStack")
|
per_mesh_stack = node.callDecoration("getStack")
|
||||||
|
|
||||||
# Get color to render this mesh in from ExtrudersModel
|
extruder_index = int(node.callDecoration("getActiveExtruderPosition"))
|
||||||
extruder_index = 0
|
|
||||||
extruder_id = node.callDecoration("getActiveExtruder")
|
|
||||||
if extruder_id:
|
|
||||||
extruder_index = max(0, self._extruders_model.find("id", extruder_id))
|
|
||||||
|
|
||||||
# Use the support extruder instead of the active extruder if this is a support_mesh
|
# Use the support extruder instead of the active extruder if this is a support_mesh
|
||||||
if per_mesh_stack:
|
if per_mesh_stack:
|
||||||
if per_mesh_stack.getProperty("support_mesh", "value"):
|
if per_mesh_stack.getProperty("support_mesh", "value"):
|
||||||
extruder_index = int(global_container_stack.getProperty("support_extruder_nr", "value"))
|
extruder_index = int(global_container_stack.getExtruderPositionValueWithDefault("support_extruder_nr"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
material_color = self._extruders_model.getItem(extruder_index)["color"]
|
material_color = self._extruders_model.getItem(extruder_index)["color"]
|
||||||
|
|
|
@ -1,39 +1,101 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from UM.Math.Vector import Vector
|
|
||||||
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
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt, QTimer
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
|
||||||
|
from UM.Math.Vector import Vector
|
||||||
|
from UM.Tool import Tool
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.Event import Event, MouseEvent
|
||||||
|
|
||||||
|
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||||
|
from UM.Scene.Selection import Selection
|
||||||
|
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||||
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
|
|
||||||
|
from cura.PickingPass import PickingPass
|
||||||
|
|
||||||
|
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
||||||
|
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
||||||
|
|
||||||
|
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||||
|
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||||
|
from UM.Scene.GroupDecorator import GroupDecorator
|
||||||
|
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||||
|
|
||||||
|
from UM.Settings.SettingInstance import SettingInstance
|
||||||
|
|
||||||
class SupportEraser(Tool):
|
class SupportEraser(Tool):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._shortcut_key = Qt.Key_G
|
self._shortcut_key = Qt.Key_G
|
||||||
self._controller = Application.getInstance().getController()
|
self._controller = self.getController()
|
||||||
|
|
||||||
|
self._selection_pass = None
|
||||||
|
Application.getInstance().globalContainerStackChanged.connect(self._updateEnabled)
|
||||||
|
|
||||||
|
# Note: if the selection is cleared with this tool active, there is no way to switch to
|
||||||
|
# another tool than to reselect an object (by clicking it) because the tool buttons in the
|
||||||
|
# toolbar will have been disabled. That is why we need to ignore the first press event
|
||||||
|
# after the selection has been cleared.
|
||||||
|
Selection.selectionChanged.connect(self._onSelectionChanged)
|
||||||
|
self._had_selection = False
|
||||||
|
self._skip_press = False
|
||||||
|
|
||||||
|
self._had_selection_timer = QTimer()
|
||||||
|
self._had_selection_timer.setInterval(0)
|
||||||
|
self._had_selection_timer.setSingleShot(True)
|
||||||
|
self._had_selection_timer.timeout.connect(self._selectionChangeDelay)
|
||||||
|
|
||||||
def event(self, event):
|
def event(self, event):
|
||||||
super().event(event)
|
super().event(event)
|
||||||
|
modifiers = QApplication.keyboardModifiers()
|
||||||
|
ctrl_is_active = modifiers & Qt.ControlModifier
|
||||||
|
|
||||||
if event.type == Event.ToolActivateEvent:
|
if event.type == Event.MousePressEvent and self._controller.getToolsEnabled():
|
||||||
|
if ctrl_is_active:
|
||||||
|
self._controller.setActiveTool("TranslateTool")
|
||||||
|
return
|
||||||
|
|
||||||
# Load the remover mesh:
|
if self._skip_press:
|
||||||
self._createEraserMesh()
|
# The selection was previously cleared, do not add/remove an anti-support mesh but
|
||||||
|
# use this click for selection and reactivating this tool only.
|
||||||
|
self._skip_press = False
|
||||||
|
return
|
||||||
|
|
||||||
# After we load the mesh, deactivate the tool again:
|
if self._selection_pass is None:
|
||||||
self.getController().setActiveTool(None)
|
# The selection renderpass is used to identify objects in the current view
|
||||||
|
self._selection_pass = Application.getInstance().getRenderer().getRenderPass("selection")
|
||||||
|
picked_node = self._controller.getScene().findObject(self._selection_pass.getIdAtPosition(event.x, event.y))
|
||||||
|
if not picked_node:
|
||||||
|
# There is no slicable object at the picked location
|
||||||
|
return
|
||||||
|
|
||||||
def _createEraserMesh(self):
|
node_stack = picked_node.callDecoration("getStack")
|
||||||
|
if node_stack:
|
||||||
|
if node_stack.getProperty("anti_overhang_mesh", "value"):
|
||||||
|
self._removeEraserMesh(picked_node)
|
||||||
|
return
|
||||||
|
|
||||||
|
elif node_stack.getProperty("support_mesh", "value") or node_stack.getProperty("infill_mesh", "value") or node_stack.getProperty("cutting_mesh", "value"):
|
||||||
|
# Only "normal" meshes can have anti_overhang_meshes added to them
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create a pass for picking a world-space location from the mouse location
|
||||||
|
active_camera = self._controller.getScene().getActiveCamera()
|
||||||
|
picking_pass = PickingPass(active_camera.getViewportWidth(), active_camera.getViewportHeight())
|
||||||
|
picking_pass.render()
|
||||||
|
|
||||||
|
picked_position = picking_pass.getPickedPosition(event.x, event.y)
|
||||||
|
|
||||||
|
# Add the anti_overhang_mesh cube at the picked location
|
||||||
|
self._createEraserMesh(picked_node, picked_position)
|
||||||
|
|
||||||
|
def _createEraserMesh(self, parent: CuraSceneNode, position: Vector):
|
||||||
node = CuraSceneNode()
|
node = CuraSceneNode()
|
||||||
|
|
||||||
node.setName("Eraser")
|
node.setName("Eraser")
|
||||||
|
@ -41,31 +103,61 @@ class SupportEraser(Tool):
|
||||||
mesh = MeshBuilder()
|
mesh = MeshBuilder()
|
||||||
mesh.addCube(10,10,10)
|
mesh.addCube(10,10,10)
|
||||||
node.setMeshData(mesh.build())
|
node.setMeshData(mesh.build())
|
||||||
# Place the cube in the platform. Do it manually so it works if the "automatic drop models" is OFF
|
|
||||||
move_vector = Vector(0, 5, 0)
|
|
||||||
node.setPosition(move_vector)
|
|
||||||
|
|
||||||
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
||||||
|
|
||||||
node.addDecorator(SettingOverrideDecorator())
|
|
||||||
node.addDecorator(BuildPlateDecorator(active_build_plate))
|
node.addDecorator(BuildPlateDecorator(active_build_plate))
|
||||||
node.addDecorator(SliceableObjectDecorator())
|
node.addDecorator(SliceableObjectDecorator())
|
||||||
|
|
||||||
stack = node.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
|
stack = node.callDecoration("getStack") # created by SettingOverrideDecorator that is automatically added to CuraSceneNode
|
||||||
if not stack:
|
|
||||||
node.addDecorator(SettingOverrideDecorator())
|
|
||||||
stack = node.callDecoration("getStack")
|
|
||||||
|
|
||||||
settings = stack.getTop()
|
settings = stack.getTop()
|
||||||
|
|
||||||
if not (settings.getInstance("anti_overhang_mesh") and settings.getProperty("anti_overhang_mesh", "value")):
|
|
||||||
definition = stack.getSettingDefinition("anti_overhang_mesh")
|
definition = stack.getSettingDefinition("anti_overhang_mesh")
|
||||||
new_instance = SettingInstance(definition, settings)
|
new_instance = SettingInstance(definition, settings)
|
||||||
new_instance.setProperty("value", True)
|
new_instance.setProperty("value", True)
|
||||||
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
||||||
settings.addInstance(new_instance)
|
settings.addInstance(new_instance)
|
||||||
|
|
||||||
scene = self._controller.getScene()
|
op = AddSceneNodeOperation(node, parent)
|
||||||
op = AddSceneNodeOperation(node, scene.getRoot())
|
|
||||||
op.push()
|
op.push()
|
||||||
|
node.setPosition(position, CuraSceneNode.TransformSpace.World)
|
||||||
|
|
||||||
Application.getInstance().getController().getScene().sceneChanged.emit(node)
|
Application.getInstance().getController().getScene().sceneChanged.emit(node)
|
||||||
|
|
||||||
|
def _removeEraserMesh(self, node: CuraSceneNode):
|
||||||
|
parent = node.getParent()
|
||||||
|
if parent == self._controller.getScene().getRoot():
|
||||||
|
parent = None
|
||||||
|
|
||||||
|
op = RemoveSceneNodeOperation(node)
|
||||||
|
op.push()
|
||||||
|
|
||||||
|
if parent and not Selection.isSelected(parent):
|
||||||
|
Selection.add(parent)
|
||||||
|
|
||||||
|
Application.getInstance().getController().getScene().sceneChanged.emit(node)
|
||||||
|
|
||||||
|
def _updateEnabled(self):
|
||||||
|
plugin_enabled = False
|
||||||
|
|
||||||
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if global_container_stack:
|
||||||
|
plugin_enabled = global_container_stack.getProperty("anti_overhang_mesh", "enabled")
|
||||||
|
|
||||||
|
Application.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, plugin_enabled)
|
||||||
|
|
||||||
|
def _onSelectionChanged(self):
|
||||||
|
# When selection is passed from one object to another object, first the selection is cleared
|
||||||
|
# and then it is set to the new object. We are only interested in the change from no selection
|
||||||
|
# to a selection or vice-versa, not in a change from one object to another. A timer is used to
|
||||||
|
# "merge" a possible clear/select action in a single frame
|
||||||
|
if Selection.hasSelection() != self._had_selection:
|
||||||
|
self._had_selection_timer.start()
|
||||||
|
|
||||||
|
def _selectionChangeDelay(self):
|
||||||
|
has_selection = Selection.hasSelection()
|
||||||
|
if not has_selection and self._had_selection:
|
||||||
|
self._skip_press = True
|
||||||
|
else:
|
||||||
|
self._skip_press = False
|
||||||
|
|
||||||
|
self._had_selection = has_selection
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from UM.FileHandler.FileWriter import FileWriter #To choose based on the output file mode (text vs. binary).
|
||||||
|
from UM.FileHandler.WriteFileJob import WriteFileJob #To call the file writer asynchronously.
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
from UM.Qt.Duration import Duration, DurationFormat
|
from UM.Qt.Duration import Duration, DurationFormat
|
||||||
|
from UM.OutputDevice import OutputDeviceError #To show that something went wrong when writing.
|
||||||
|
from UM.Scene.SceneNode import SceneNode #For typing.
|
||||||
|
from UM.Version import Version #To check against firmware versions for support.
|
||||||
|
|
||||||
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState
|
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||||
|
@ -20,10 +25,11 @@ from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
|
||||||
from PyQt5.QtGui import QDesktopServices
|
from PyQt5.QtGui import QDesktopServices
|
||||||
from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject
|
from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject
|
||||||
|
|
||||||
from time import time
|
from time import time, sleep
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional, Dict, List
|
||||||
|
|
||||||
|
import io #To create the correct buffers for sending data to the printer.
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
@ -79,26 +85,50 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
|
||||||
|
|
||||||
self._latest_reply_handler = None
|
self._latest_reply_handler = None
|
||||||
|
|
||||||
|
def requestWrite(self, nodes: List[SceneNode], file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
|
||||||
def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
|
|
||||||
self.writeStarted.emit(self)
|
self.writeStarted.emit(self)
|
||||||
|
|
||||||
gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict", [])
|
#Formats supported by this application (file types that we can actually write).
|
||||||
active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
if file_handler:
|
||||||
gcode_list = gcode_dict[active_build_plate_id]
|
file_formats = file_handler.getSupportedFileTypesWrite()
|
||||||
|
|
||||||
if not gcode_list:
|
|
||||||
# Unable to find g-code. Nothing to send
|
|
||||||
return
|
|
||||||
|
|
||||||
self._gcode = gcode_list
|
|
||||||
|
|
||||||
if len(self._printers) > 1:
|
|
||||||
self._spawnPrinterSelectionDialog()
|
|
||||||
else:
|
else:
|
||||||
self.sendPrintJob()
|
file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite()
|
||||||
|
|
||||||
|
#Create a list from the supported file formats string.
|
||||||
|
machine_file_formats = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("file_formats").split(";")
|
||||||
|
machine_file_formats = [file_type.strip() for file_type in machine_file_formats]
|
||||||
|
#Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format.
|
||||||
|
if "application/x-ufp" not in machine_file_formats and self.printerType == "ultimaker3" and Version(self.firmwareVersion) >= Version("4.4"):
|
||||||
|
machine_file_formats = ["application/x-ufp"] + machine_file_formats
|
||||||
|
|
||||||
|
# Take the intersection between file_formats and machine_file_formats.
|
||||||
|
format_by_mimetype = {format["mime_type"]: format for format in file_formats}
|
||||||
|
file_formats = [format_by_mimetype[mimetype] for mimetype in machine_file_formats] #Keep them ordered according to the preference in machine_file_formats.
|
||||||
|
|
||||||
|
if len(file_formats) == 0:
|
||||||
|
Logger.log("e", "There are no file formats available to write with!")
|
||||||
|
raise OutputDeviceError.WriteRequestFailedError(i18n_catalog.i18nc("@info:status", "There are no file formats available to write with!"))
|
||||||
|
preferred_format = file_formats[0]
|
||||||
|
|
||||||
|
#Just take the first file format available.
|
||||||
|
if file_handler is not None:
|
||||||
|
writer = file_handler.getWriterByMimeType(preferred_format["mime_type"])
|
||||||
|
else:
|
||||||
|
writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(preferred_format["mime_type"])
|
||||||
|
|
||||||
|
#This function pauses with the yield, waiting on instructions on which printer it needs to print with.
|
||||||
|
self._sending_job = self._sendPrintJob(writer, preferred_format, nodes)
|
||||||
|
self._sending_job.send(None) #Start the generator.
|
||||||
|
|
||||||
|
if len(self._printers) > 1: #We need to ask the user.
|
||||||
|
self._spawnPrinterSelectionDialog()
|
||||||
|
is_job_sent = True
|
||||||
|
else: #Just immediately continue.
|
||||||
|
self._sending_job.send("") #No specifically selected printer.
|
||||||
|
is_job_sent = self._sending_job.send(None)
|
||||||
|
|
||||||
# Notify the UI that a switch to the print monitor should happen
|
# Notify the UI that a switch to the print monitor should happen
|
||||||
|
if is_job_sent:
|
||||||
Application.getInstance().getController().setActiveStage("MonitorStage")
|
Application.getInstance().getController().setActiveStage("MonitorStage")
|
||||||
|
|
||||||
def _spawnPrinterSelectionDialog(self):
|
def _spawnPrinterSelectionDialog(self):
|
||||||
|
@ -112,29 +142,54 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
|
||||||
def clusterSize(self):
|
def clusterSize(self):
|
||||||
return self._cluster_size
|
return self._cluster_size
|
||||||
|
|
||||||
@pyqtSlot()
|
## Allows the user to choose a printer to print with from the printer
|
||||||
|
# selection dialogue.
|
||||||
|
# \param target_printer The name of the printer to target.
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def sendPrintJob(self, target_printer = ""):
|
def selectPrinter(self, target_printer: str = "") -> None:
|
||||||
|
self._sending_job.send(target_printer)
|
||||||
|
|
||||||
|
## Greenlet to send a job to the printer over the network.
|
||||||
|
#
|
||||||
|
# This greenlet gets called asynchronously in requestWrite. It is a
|
||||||
|
# greenlet in order to optionally wait for selectPrinter() to select a
|
||||||
|
# printer.
|
||||||
|
# The greenlet yields exactly three times: First time None,
|
||||||
|
# \param writer The file writer to use to create the data.
|
||||||
|
# \param preferred_format A dictionary containing some information about
|
||||||
|
# what format to write to. This is necessary to create the correct buffer
|
||||||
|
# types and file extension and such.
|
||||||
|
def _sendPrintJob(self, writer: FileWriter, preferred_format: Dict, nodes: List[SceneNode]):
|
||||||
Logger.log("i", "Sending print job to printer.")
|
Logger.log("i", "Sending print job to printer.")
|
||||||
if self._sending_gcode:
|
if self._sending_gcode:
|
||||||
self._error_message = Message(
|
self._error_message = Message(
|
||||||
i18n_catalog.i18nc("@info:status",
|
i18n_catalog.i18nc("@info:status",
|
||||||
"Sending new jobs (temporarily) blocked, still sending the previous print job."))
|
"Sending new jobs (temporarily) blocked, still sending the previous print job."))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
return
|
yield #Wait on the user to select a target printer.
|
||||||
|
yield #Wait for the write job to be finished.
|
||||||
|
yield False #Return whether this was a success or not.
|
||||||
|
yield #Prevent StopIteration.
|
||||||
|
|
||||||
self._sending_gcode = True
|
self._sending_gcode = True
|
||||||
|
|
||||||
self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1,
|
target_printer = yield #Potentially wait on the user to select a target printer.
|
||||||
i18n_catalog.i18nc("@info:title", "Sending Data"))
|
|
||||||
self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
|
# Using buffering greatly reduces the write time for many lines of gcode
|
||||||
|
if preferred_format["mode"] == FileWriter.OutputMode.TextMode:
|
||||||
|
stream = io.StringIO()
|
||||||
|
else: #Binary mode.
|
||||||
|
stream = io.BytesIO()
|
||||||
|
|
||||||
|
job = WriteFileJob(writer, stream, nodes, preferred_format["mode"])
|
||||||
|
|
||||||
|
self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1,
|
||||||
|
title = i18n_catalog.i18nc("@info:title", "Sending Data"))
|
||||||
|
self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), icon = None, description = "")
|
||||||
self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
|
self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
|
||||||
self._progress_message.show()
|
self._progress_message.show()
|
||||||
|
|
||||||
compressed_gcode = self._compressGCode()
|
job.start()
|
||||||
if compressed_gcode is None:
|
|
||||||
# Abort was called.
|
|
||||||
return
|
|
||||||
|
|
||||||
parts = []
|
parts = []
|
||||||
|
|
||||||
|
@ -146,18 +201,27 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
|
||||||
# Add user name to the print_job
|
# Add user name to the print_job
|
||||||
parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"))
|
parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"))
|
||||||
|
|
||||||
file_name = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName
|
file_name = Application.getInstance().getPrintInformation().jobName + "." + preferred_format["extension"]
|
||||||
|
|
||||||
parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, compressed_gcode))
|
while not job.isFinished():
|
||||||
|
sleep(0.1)
|
||||||
|
output = stream.getvalue() #Either str or bytes depending on the output mode.
|
||||||
|
if isinstance(stream, io.StringIO):
|
||||||
|
output = output.encode("utf-8")
|
||||||
|
|
||||||
|
parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, output))
|
||||||
|
|
||||||
self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts, onFinished=self._onPostPrintJobFinished, onProgress=self._onUploadPrintJobProgress)
|
self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts, onFinished=self._onPostPrintJobFinished, onProgress=self._onUploadPrintJobProgress)
|
||||||
|
|
||||||
|
yield True #Return that we had success!
|
||||||
|
yield #To prevent having to catch the StopIteration exception.
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify=activePrinterChanged)
|
@pyqtProperty(QObject, notify=activePrinterChanged)
|
||||||
def activePrinter(self) -> Optional["PrinterOutputModel"]:
|
def activePrinter(self) -> Optional[PrinterOutputModel]:
|
||||||
return self._active_printer
|
return self._active_printer
|
||||||
|
|
||||||
@pyqtSlot(QObject)
|
@pyqtSlot(QObject)
|
||||||
def setActivePrinter(self, printer):
|
def setActivePrinter(self, printer: Optional[PrinterOutputModel]):
|
||||||
if self._active_printer != printer:
|
if self._active_printer != printer:
|
||||||
if self._active_printer and self._active_printer.camera:
|
if self._active_printer and self._active_printer.camera:
|
||||||
self._active_printer.camera.stop()
|
self._active_printer.camera.stop()
|
||||||
|
@ -169,7 +233,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
|
||||||
self._compressing_gcode = False
|
self._compressing_gcode = False
|
||||||
self._sending_gcode = False
|
self._sending_gcode = False
|
||||||
|
|
||||||
def _onUploadPrintJobProgress(self, bytes_sent, bytes_total):
|
def _onUploadPrintJobProgress(self, bytes_sent:int, bytes_total:int):
|
||||||
if bytes_total > 0:
|
if bytes_total > 0:
|
||||||
new_progress = bytes_sent / bytes_total * 100
|
new_progress = bytes_sent / bytes_total * 100
|
||||||
# Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
|
# Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
|
||||||
|
@ -182,7 +246,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
|
||||||
self._progress_message.setProgress(0)
|
self._progress_message.setProgress(0)
|
||||||
self._progress_message.hide()
|
self._progress_message.hide()
|
||||||
|
|
||||||
def _progressMessageActionTriggered(self, message_id=None, action_id=None):
|
def _progressMessageActionTriggered(self, message_id: Optional[str]=None, action_id: Optional[str]=None) -> None:
|
||||||
if action_id == "Abort":
|
if action_id == "Abort":
|
||||||
Logger.log("d", "User aborted sending print to remote.")
|
Logger.log("d", "User aborted sending print to remote.")
|
||||||
self._progress_message.hide()
|
self._progress_message.hide()
|
||||||
|
@ -194,32 +258,33 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
|
||||||
# the "reply" should be disconnected
|
# the "reply" should be disconnected
|
||||||
if self._latest_reply_handler:
|
if self._latest_reply_handler:
|
||||||
self._latest_reply_handler.disconnect()
|
self._latest_reply_handler.disconnect()
|
||||||
|
self._latest_reply_handler = None
|
||||||
|
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def openPrintJobControlPanel(self):
|
def openPrintJobControlPanel(self) -> None:
|
||||||
Logger.log("d", "Opening print job control panel...")
|
Logger.log("d", "Opening print job control panel...")
|
||||||
QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs"))
|
QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs"))
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def openPrinterControlPanel(self):
|
def openPrinterControlPanel(self) -> None:
|
||||||
Logger.log("d", "Opening printer control panel...")
|
Logger.log("d", "Opening printer control panel...")
|
||||||
QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers"))
|
QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers"))
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify=printJobsChanged)
|
@pyqtProperty("QVariantList", notify=printJobsChanged)
|
||||||
def printJobs(self):
|
def printJobs(self)-> List[PrintJobOutputModel] :
|
||||||
return self._print_jobs
|
return self._print_jobs
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify=printJobsChanged)
|
@pyqtProperty("QVariantList", notify=printJobsChanged)
|
||||||
def queuedPrintJobs(self):
|
def queuedPrintJobs(self) -> List[PrintJobOutputModel]:
|
||||||
return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is None or print_job.state == "queued"]
|
return [print_job for print_job in self._print_jobs if print_job.state == "queued"]
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify=printJobsChanged)
|
@pyqtProperty("QVariantList", notify=printJobsChanged)
|
||||||
def activePrintJobs(self):
|
def activePrintJobs(self) -> List[PrintJobOutputModel]:
|
||||||
return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None and print_job.state != "queued"]
|
return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None and print_job.state != "queued"]
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify=clusterPrintersChanged)
|
@pyqtProperty("QVariantList", notify=clusterPrintersChanged)
|
||||||
def connectedPrintersTypeCount(self):
|
def connectedPrintersTypeCount(self) -> List[PrinterOutputModel]:
|
||||||
printer_count = {}
|
printer_count = {}
|
||||||
for printer in self._printers:
|
for printer in self._printers:
|
||||||
if printer.type in printer_count:
|
if printer.type in printer_count:
|
||||||
|
@ -232,22 +297,22 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@pyqtSlot(int, result=str)
|
@pyqtSlot(int, result=str)
|
||||||
def formatDuration(self, seconds):
|
def formatDuration(self, seconds: int) -> str:
|
||||||
return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
|
return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
|
||||||
|
|
||||||
@pyqtSlot(int, result=str)
|
@pyqtSlot(int, result=str)
|
||||||
def getTimeCompleted(self, time_remaining):
|
def getTimeCompleted(self, time_remaining: int) -> str:
|
||||||
current_time = time()
|
current_time = time()
|
||||||
datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
|
datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
|
||||||
return "{hour:02d}:{minute:02d}".format(hour=datetime_completed.hour, minute=datetime_completed.minute)
|
return "{hour:02d}:{minute:02d}".format(hour=datetime_completed.hour, minute=datetime_completed.minute)
|
||||||
|
|
||||||
@pyqtSlot(int, result=str)
|
@pyqtSlot(int, result=str)
|
||||||
def getDateCompleted(self, time_remaining):
|
def getDateCompleted(self, time_remaining: int) -> str:
|
||||||
current_time = time()
|
current_time = time()
|
||||||
datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
|
datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
|
||||||
return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper()
|
return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper()
|
||||||
|
|
||||||
def _printJobStateChanged(self):
|
def _printJobStateChanged(self) -> None:
|
||||||
username = self._getUserName()
|
username = self._getUserName()
|
||||||
|
|
||||||
if username is None:
|
if username is None:
|
||||||
|
@ -270,13 +335,13 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
|
||||||
# Keep a list of all completed jobs so we know if something changed next time.
|
# Keep a list of all completed jobs so we know if something changed next time.
|
||||||
self._finished_jobs = finished_jobs
|
self._finished_jobs = finished_jobs
|
||||||
|
|
||||||
def _update(self):
|
def _update(self) -> None:
|
||||||
if not super()._update():
|
if not super()._update():
|
||||||
return
|
return
|
||||||
self.get("printers/", onFinished=self._onGetPrintersDataFinished)
|
self.get("printers/", onFinished=self._onGetPrintersDataFinished)
|
||||||
self.get("print_jobs/", onFinished=self._onGetPrintJobsFinished)
|
self.get("print_jobs/", onFinished=self._onGetPrintJobsFinished)
|
||||||
|
|
||||||
def _onGetPrintJobsFinished(self, reply: QNetworkReply):
|
def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None:
|
||||||
if not checkValidGetReply(reply):
|
if not checkValidGetReply(reply):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -296,8 +361,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
|
||||||
self._updatePrintJob(print_job, print_job_data)
|
self._updatePrintJob(print_job, print_job_data)
|
||||||
|
|
||||||
if print_job.state != "queued": # Print job should be assigned to a printer.
|
if print_job.state != "queued": # Print job should be assigned to a printer.
|
||||||
if print_job.state == "failed":
|
if print_job.state in ["failed", "finished", "aborted"]:
|
||||||
# Print job was failed, so don't attach it to a printer.
|
# Print job was already completed, so don't attach it to a printer.
|
||||||
printer = None
|
printer = None
|
||||||
else:
|
else:
|
||||||
printer = self._getPrinterByKey(print_job_data["printer_uuid"])
|
printer = self._getPrinterByKey(print_job_data["printer_uuid"])
|
||||||
|
@ -318,7 +383,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
|
||||||
if job_list_changed:
|
if job_list_changed:
|
||||||
self.printJobsChanged.emit() # Do a single emit for all print job changes.
|
self.printJobsChanged.emit() # Do a single emit for all print job changes.
|
||||||
|
|
||||||
def _onGetPrintersDataFinished(self, reply: QNetworkReply):
|
def _onGetPrintersDataFinished(self, reply: QNetworkReply) -> None:
|
||||||
if not checkValidGetReply(reply):
|
if not checkValidGetReply(reply):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -347,34 +412,45 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
|
||||||
if removed_printers or printer_list_changed:
|
if removed_printers or printer_list_changed:
|
||||||
self.printersChanged.emit()
|
self.printersChanged.emit()
|
||||||
|
|
||||||
def _createPrinterModel(self, data):
|
def _createPrinterModel(self, data: Dict) -> PrinterOutputModel:
|
||||||
printer = PrinterOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
|
printer = PrinterOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
|
||||||
number_of_extruders=self._number_of_extruders)
|
number_of_extruders=self._number_of_extruders)
|
||||||
printer.setCamera(NetworkCamera("http://" + data["ip_address"] + ":8080/?action=stream"))
|
printer.setCamera(NetworkCamera("http://" + data["ip_address"] + ":8080/?action=stream"))
|
||||||
self._printers.append(printer)
|
self._printers.append(printer)
|
||||||
return printer
|
return printer
|
||||||
|
|
||||||
def _createPrintJobModel(self, data):
|
def _createPrintJobModel(self, data: Dict) -> PrintJobOutputModel:
|
||||||
print_job = PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
|
print_job = PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
|
||||||
key=data["uuid"], name= data["name"])
|
key=data["uuid"], name= data["name"])
|
||||||
print_job.stateChanged.connect(self._printJobStateChanged)
|
print_job.stateChanged.connect(self._printJobStateChanged)
|
||||||
self._print_jobs.append(print_job)
|
self._print_jobs.append(print_job)
|
||||||
return print_job
|
return print_job
|
||||||
|
|
||||||
def _updatePrintJob(self, print_job, data):
|
def _updatePrintJob(self, print_job: PrintJobOutputModel, data: Dict) -> None:
|
||||||
print_job.updateTimeTotal(data["time_total"])
|
print_job.updateTimeTotal(data["time_total"])
|
||||||
print_job.updateTimeElapsed(data["time_elapsed"])
|
print_job.updateTimeElapsed(data["time_elapsed"])
|
||||||
print_job.updateState(data["status"])
|
print_job.updateState(data["status"])
|
||||||
print_job.updateOwner(data["owner"])
|
print_job.updateOwner(data["owner"])
|
||||||
|
|
||||||
def _updatePrinter(self, printer, data):
|
def _updatePrinter(self, printer: PrinterOutputModel, data: Dict) -> None:
|
||||||
# For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer.
|
# For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer.
|
||||||
# Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping.
|
# Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping.
|
||||||
self._printer_uuid_to_unique_name_mapping[data["uuid"]] = data["unique_name"]
|
self._printer_uuid_to_unique_name_mapping[data["uuid"]] = data["unique_name"]
|
||||||
|
|
||||||
|
definitions = ContainerRegistry.getInstance().findDefinitionContainers(name = data["machine_variant"])
|
||||||
|
if not definitions:
|
||||||
|
Logger.log("w", "Unable to find definition for machine variant %s", data["machine_variant"])
|
||||||
|
return
|
||||||
|
|
||||||
|
machine_definition = definitions[0]
|
||||||
|
|
||||||
printer.updateName(data["friendly_name"])
|
printer.updateName(data["friendly_name"])
|
||||||
printer.updateKey(data["uuid"])
|
printer.updateKey(data["uuid"])
|
||||||
printer.updateType(data["machine_variant"])
|
printer.updateType(data["machine_variant"])
|
||||||
|
|
||||||
|
# Do not store the buildplate information that comes from connect if the current printer has not buildplate information
|
||||||
|
if "build_plate" in data and machine_definition.getMetaDataEntry("has_variant_buildplates", False):
|
||||||
|
printer.updateBuildplateName(data["build_plate"]["type"])
|
||||||
if not data["enabled"]:
|
if not data["enabled"]:
|
||||||
printer.updateState("disabled")
|
printer.updateState("disabled")
|
||||||
else:
|
else:
|
||||||
|
@ -411,7 +487,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
|
||||||
brand=brand, color=color, name=name)
|
brand=brand, color=color, name=name)
|
||||||
extruder.updateActiveMaterial(material)
|
extruder.updateActiveMaterial(material)
|
||||||
|
|
||||||
def _removeJob(self, job):
|
def _removeJob(self, job: PrintJobOutputModel):
|
||||||
if job not in self._print_jobs:
|
if job not in self._print_jobs:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -422,7 +498,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _removePrinter(self, printer):
|
def _removePrinter(self, printer: PrinterOutputModel):
|
||||||
self._printers.remove(printer)
|
self._printers.remove(printer)
|
||||||
if self._active_printer == printer:
|
if self._active_printer == printer:
|
||||||
self._active_printer = None
|
self._active_printer = None
|
||||||
|
|
|
@ -13,7 +13,9 @@ class ClusterUM3PrinterOutputController(PrinterOutputController):
|
||||||
def __init__(self, output_device):
|
def __init__(self, output_device):
|
||||||
super().__init__(output_device)
|
super().__init__(output_device)
|
||||||
self.can_pre_heat_bed = False
|
self.can_pre_heat_bed = False
|
||||||
|
self.can_pre_heat_hotends = False
|
||||||
self.can_control_manually = False
|
self.can_control_manually = False
|
||||||
|
self.can_send_raw_gcode = False
|
||||||
|
|
||||||
def setJobState(self, job: "PrintJobOutputModel", state: str):
|
def setJobState(self, job: "PrintJobOutputModel", state: str):
|
||||||
data = "{\"action\": \"%s\"}" % state
|
data = "{\"action\": \"%s\"}" % state
|
||||||
|
|
|
@ -97,6 +97,25 @@ class DiscoverUM3Action(MachineAction):
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def setGroupName(self, group_name):
|
||||||
|
Logger.log("d", "Attempting to set the group name of the active machine to %s", group_name)
|
||||||
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if global_container_stack:
|
||||||
|
meta_data = global_container_stack.getMetaData()
|
||||||
|
if "connect_group_name" in meta_data:
|
||||||
|
previous_connect_group_name = meta_data["connect_group_name"]
|
||||||
|
global_container_stack.setMetaDataEntry("connect_group_name", group_name)
|
||||||
|
# Find all the places where there is the same group name and change it accordingly
|
||||||
|
Application.getInstance().getMachineManager().replaceContainersMetadata(key = "connect_group_name", value = previous_connect_group_name, new_value = group_name)
|
||||||
|
else:
|
||||||
|
global_container_stack.addMetaDataEntry("connect_group_name", group_name)
|
||||||
|
global_container_stack.addMetaDataEntry("hidden", False)
|
||||||
|
|
||||||
|
if self._network_plugin:
|
||||||
|
# Ensure that the connection states are refreshed.
|
||||||
|
self._network_plugin.reCheckConnections()
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def setKey(self, key):
|
def setKey(self, key):
|
||||||
Logger.log("d", "Attempting to set the network key of the active machine to %s", key)
|
Logger.log("d", "Attempting to set the network key of the active machine to %s", key)
|
||||||
|
@ -104,11 +123,13 @@ class DiscoverUM3Action(MachineAction):
|
||||||
if global_container_stack:
|
if global_container_stack:
|
||||||
meta_data = global_container_stack.getMetaData()
|
meta_data = global_container_stack.getMetaData()
|
||||||
if "um_network_key" in meta_data:
|
if "um_network_key" in meta_data:
|
||||||
|
previous_network_key= meta_data["um_network_key"]
|
||||||
global_container_stack.setMetaDataEntry("um_network_key", key)
|
global_container_stack.setMetaDataEntry("um_network_key", key)
|
||||||
# Delete old authentication data.
|
# Delete old authentication data.
|
||||||
Logger.log("d", "Removing old authentication id %s for device %s", global_container_stack.getMetaDataEntry("network_authentication_id", None), key)
|
Logger.log("d", "Removing old authentication id %s for device %s", global_container_stack.getMetaDataEntry("network_authentication_id", None), key)
|
||||||
global_container_stack.removeMetaDataEntry("network_authentication_id")
|
global_container_stack.removeMetaDataEntry("network_authentication_id")
|
||||||
global_container_stack.removeMetaDataEntry("network_authentication_key")
|
global_container_stack.removeMetaDataEntry("network_authentication_key")
|
||||||
|
Application.getInstance().getMachineManager().replaceContainersMetadata(key = "um_network_key", value = previous_network_key, new_value = key)
|
||||||
else:
|
else:
|
||||||
global_container_stack.addMetaDataEntry("um_network_key", key)
|
global_container_stack.addMetaDataEntry("um_network_key", key)
|
||||||
|
|
||||||
|
@ -126,6 +147,10 @@ class DiscoverUM3Action(MachineAction):
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
@pyqtSlot(str, result = bool)
|
||||||
|
def existsKey(self, key) -> bool:
|
||||||
|
return Application.getInstance().getMachineManager().existNetworkInstances(network_key = key)
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def loadConfigurationFromPrinter(self):
|
def loadConfigurationFromPrinter(self):
|
||||||
machine_manager = Application.getInstance().getMachineManager()
|
machine_manager = Application.getInstance().getMachineManager()
|
||||||
|
|
|
@ -5,6 +5,7 @@ import QtQuick 2.2
|
||||||
import QtQuick.Controls 1.1
|
import QtQuick.Controls 1.1
|
||||||
import QtQuick.Layouts 1.1
|
import QtQuick.Layouts 1.1
|
||||||
import QtQuick.Window 2.1
|
import QtQuick.Window 2.1
|
||||||
|
import QtQuick.Dialogs 1.2
|
||||||
|
|
||||||
Cura.MachineAction
|
Cura.MachineAction
|
||||||
{
|
{
|
||||||
|
@ -32,13 +33,33 @@ Cura.MachineAction
|
||||||
if(base.selectedDevice && base.completeProperties)
|
if(base.selectedDevice && base.completeProperties)
|
||||||
{
|
{
|
||||||
var printerKey = base.selectedDevice.key
|
var printerKey = base.selectedDevice.key
|
||||||
|
var printerName = base.selectedDevice.name // TODO To change when the groups have a name
|
||||||
if (manager.getStoredKey() != printerKey)
|
if (manager.getStoredKey() != printerKey)
|
||||||
{
|
{
|
||||||
manager.setKey(printerKey);
|
// Check if there is another instance with the same key
|
||||||
completed();
|
if (!manager.existsKey(printerKey))
|
||||||
|
{
|
||||||
|
manager.setKey(printerKey)
|
||||||
|
manager.setGroupName(printerName) // TODO To change when the groups have a name
|
||||||
|
completed()
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existingConnectionDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageDialog
|
||||||
|
{
|
||||||
|
id: existingConnectionDialog
|
||||||
|
title: catalog.i18nc("@window:title", "Existing Connection")
|
||||||
|
icon: StandardIcon.Information
|
||||||
|
text: catalog.i18nc("@message:text", "This printer/group is already added to Cura. Please select another printer/group.")
|
||||||
|
standardButtons: StandardButton.Ok
|
||||||
|
modality: Qt.ApplicationModal
|
||||||
|
}
|
||||||
|
|
||||||
Column
|
Column
|
||||||
{
|
{
|
||||||
|
@ -303,7 +324,7 @@ Cura.MachineAction
|
||||||
Button
|
Button
|
||||||
{
|
{
|
||||||
text: catalog.i18nc("@action:button", "Connect")
|
text: catalog.i18nc("@action:button", "Connect")
|
||||||
enabled: (base.selectedDevice && base.completeProperties) ? true : false
|
enabled: (base.selectedDevice && base.completeProperties && base.selectedDevice.clusterSize > 0) ? true : false
|
||||||
onClicked: connectToPrinter()
|
onClicked: connectToPrinter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ class LegacyUM3PrinterOutputController(PrinterOutputController):
|
||||||
self._preheat_printer = None
|
self._preheat_printer = None
|
||||||
|
|
||||||
self.can_control_manually = False
|
self.can_control_manually = False
|
||||||
|
self.can_send_raw_gcode = False
|
||||||
|
|
||||||
# Are we still waiting for a response about preheat?
|
# Are we still waiting for a response about preheat?
|
||||||
# We need this so we can already update buttons, so it feels more snappy.
|
# We need this so we can already update buttons, so it feels more snappy.
|
||||||
|
|
|
@ -101,7 +101,7 @@ UM.Dialog
|
||||||
enabled: true
|
enabled: true
|
||||||
onClicked: {
|
onClicked: {
|
||||||
base.visible = false;
|
base.visible = false;
|
||||||
OutputDevice.sendPrintJob(printerSelectionCombobox.model.get(printerSelectionCombobox.currentIndex).key)
|
OutputDevice.selectPrinter(printerSelectionCombobox.model.get(printerSelectionCombobox.currentIndex).key)
|
||||||
// reset to defaults
|
// reset to defaults
|
||||||
printerSelectionCombobox.currentIndex = 0
|
printerSelectionCombobox.currentIndex = 0
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,17 +34,17 @@ Rectangle
|
||||||
switch (printer.state)
|
switch (printer.state)
|
||||||
{
|
{
|
||||||
case "pre_print":
|
case "pre_print":
|
||||||
return catalog.i18nc("@label", "Preparing to print")
|
return catalog.i18nc("@label:status", "Preparing to print")
|
||||||
case "printing":
|
case "printing":
|
||||||
return catalog.i18nc("@label:status", "Printing");
|
return catalog.i18nc("@label:status", "Printing");
|
||||||
case "idle":
|
case "idle":
|
||||||
return catalog.i18nc("@label:status", "Available");
|
return catalog.i18nc("@label:status", "Available");
|
||||||
case "unreachable":
|
case "unreachable":
|
||||||
return catalog.i18nc("@label:MonitorStatus", "Lost connection with the printer");
|
return catalog.i18nc("@label:status", "Lost connection with the printer");
|
||||||
case "maintenance": // TODO: new string
|
case "maintenance":
|
||||||
case "unknown":
|
return catalog.i18nc("@label:status", "Unavailable");
|
||||||
default:
|
default:
|
||||||
return catalog.i18nc("@label Printer status", "Unknown");
|
return catalog.i18nc("@label:status", "Unknown");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,9 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
|
||||||
self._zero_conf_browser.cancel()
|
self._zero_conf_browser.cancel()
|
||||||
self._zero_conf_browser = None # Force the old ServiceBrowser to be destroyed.
|
self._zero_conf_browser = None # Force the old ServiceBrowser to be destroyed.
|
||||||
|
|
||||||
|
for instance_name in list(self._discovered_devices):
|
||||||
|
self._onRemoveDevice(instance_name)
|
||||||
|
|
||||||
self._zero_conf = Zeroconf()
|
self._zero_conf = Zeroconf()
|
||||||
self._zero_conf_browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.',
|
self._zero_conf_browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.',
|
||||||
[self._appendServiceChangedRequest])
|
[self._appendServiceChangedRequest])
|
||||||
|
|
|
@ -22,6 +22,7 @@ class AutoDetectBaudJob(Job):
|
||||||
def run(self):
|
def run(self):
|
||||||
Logger.log("d", "Auto detect baud rate started.")
|
Logger.log("d", "Auto detect baud rate started.")
|
||||||
timeout = 3
|
timeout = 3
|
||||||
|
tries = 2
|
||||||
|
|
||||||
programmer = Stk500v2()
|
programmer = Stk500v2()
|
||||||
serial = None
|
serial = None
|
||||||
|
@ -31,6 +32,7 @@ class AutoDetectBaudJob(Job):
|
||||||
except:
|
except:
|
||||||
programmer.close()
|
programmer.close()
|
||||||
|
|
||||||
|
for retry in range(tries):
|
||||||
for baud_rate in self._all_baud_rates:
|
for baud_rate in self._all_baud_rates:
|
||||||
Logger.log("d", "Checking {serial} if baud rate {baud_rate} works".format(serial= self._serial_port, baud_rate = baud_rate))
|
Logger.log("d", "Checking {serial} if baud rate {baud_rate} works".format(serial= self._serial_port, baud_rate = baud_rate))
|
||||||
|
|
||||||
|
@ -63,4 +65,5 @@ class AutoDetectBaudJob(Job):
|
||||||
return
|
return
|
||||||
|
|
||||||
serial.write(b"M105\n")
|
serial.write(b"M105\n")
|
||||||
|
sleep(15) # Give the printer some time to init and try again.
|
||||||
self.setResult(None) # Unable to detect the correct baudrate.
|
self.setResult(None) # Unable to detect the correct baudrate.
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
|
||||||
from PyQt5.QtCore import QTimer
|
|
||||||
|
|
||||||
MYPY = False
|
|
||||||
if MYPY:
|
|
||||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
|
||||||
|
|
||||||
|
|
||||||
class USBPrinterOutputController(PrinterOutputController):
|
|
||||||
def __init__(self, output_device):
|
|
||||||
super().__init__(output_device)
|
|
||||||
|
|
||||||
self._preheat_bed_timer = QTimer()
|
|
||||||
self._preheat_bed_timer.setSingleShot(True)
|
|
||||||
self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished)
|
|
||||||
self._preheat_printer = None
|
|
||||||
|
|
||||||
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed):
|
|
||||||
self._output_device.sendCommand("G91")
|
|
||||||
self._output_device.sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))
|
|
||||||
self._output_device.sendCommand("G90")
|
|
||||||
|
|
||||||
def homeHead(self, printer):
|
|
||||||
self._output_device.sendCommand("G28 X")
|
|
||||||
self._output_device.sendCommand("G28 Y")
|
|
||||||
|
|
||||||
def homeBed(self, printer):
|
|
||||||
self._output_device.sendCommand("G28 Z")
|
|
||||||
|
|
||||||
def setJobState(self, job: "PrintJobOutputModel", state: str):
|
|
||||||
if state == "pause":
|
|
||||||
self._output_device.pausePrint()
|
|
||||||
job.updateState("paused")
|
|
||||||
elif state == "print":
|
|
||||||
self._output_device.resumePrint()
|
|
||||||
job.updateState("printing")
|
|
||||||
elif state == "abort":
|
|
||||||
self._output_device.cancelPrint()
|
|
||||||
pass
|
|
||||||
|
|
||||||
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration):
|
|
||||||
try:
|
|
||||||
temperature = round(temperature) # The API doesn't allow floating point.
|
|
||||||
duration = round(duration)
|
|
||||||
except ValueError:
|
|
||||||
return # Got invalid values, can't pre-heat.
|
|
||||||
|
|
||||||
self.setTargetBedTemperature(printer, temperature=temperature)
|
|
||||||
self._preheat_bed_timer.setInterval(duration * 1000)
|
|
||||||
self._preheat_bed_timer.start()
|
|
||||||
self._preheat_printer = printer
|
|
||||||
printer.updateIsPreheating(True)
|
|
||||||
|
|
||||||
def cancelPreheatBed(self, printer: "PrinterOutputModel"):
|
|
||||||
self.preheatBed(printer, temperature=0, duration=0)
|
|
||||||
self._preheat_bed_timer.stop()
|
|
||||||
printer.updateIsPreheating(False)
|
|
||||||
|
|
||||||
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int):
|
|
||||||
self._output_device.sendCommand("M140 S%s" % temperature)
|
|
||||||
|
|
||||||
def _onPreheatBedTimerFinished(self):
|
|
||||||
self.setTargetBedTemperature(self._preheat_printer, 0)
|
|
||||||
self._preheat_printer.updateIsPreheating(False)
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
@ -10,14 +10,14 @@ from UM.PluginRegistry import PluginRegistry
|
||||||
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
|
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
||||||
|
from cura.PrinterOutput.GenericOutputController import GenericOutputController
|
||||||
|
|
||||||
from .AutoDetectBaudJob import AutoDetectBaudJob
|
from .AutoDetectBaudJob import AutoDetectBaudJob
|
||||||
from .USBPrinterOutputController import USBPrinterOutputController
|
|
||||||
from .avr_isp import stk500v2, intelHex
|
from .avr_isp import stk500v2, intelHex
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty
|
from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty
|
||||||
|
|
||||||
from serial import Serial, SerialException
|
from serial import Serial, SerialException, SerialTimeoutException
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from time import time, sleep
|
from time import time, sleep
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
|
@ -116,7 +116,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def updateFirmware(self, file):
|
def updateFirmware(self, file):
|
||||||
self._firmware_location = file
|
# the file path is qurl encoded.
|
||||||
|
self._firmware_location = file.replace("file://", "")
|
||||||
self.showFirmwareInterface()
|
self.showFirmwareInterface()
|
||||||
self.setFirmwareUpdateState(FirmwareUpdateState.updating)
|
self.setFirmwareUpdateState(FirmwareUpdateState.updating)
|
||||||
self._update_firmware_thread.start()
|
self._update_firmware_thread.start()
|
||||||
|
@ -126,9 +127,11 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
||||||
if self._connection_state != ConnectionState.closed:
|
if self._connection_state != ConnectionState.closed:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
try:
|
||||||
hex_file = intelHex.readHex(self._firmware_location)
|
hex_file = intelHex.readHex(self._firmware_location)
|
||||||
if len(hex_file) == 0:
|
assert len(hex_file) > 0
|
||||||
Logger.log("e", "Unable to read provided hex file. Could not update firmware")
|
except (FileNotFoundError, AssertionError):
|
||||||
|
Logger.log("e", "Unable to read provided hex file. Could not update firmware.")
|
||||||
self.setFirmwareUpdateState(FirmwareUpdateState.firmware_not_found_error)
|
self.setFirmwareUpdateState(FirmwareUpdateState.firmware_not_found_error)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -198,7 +201,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
||||||
# Reset line number. If this is not done, first line is sometimes ignored
|
# Reset line number. If this is not done, first line is sometimes ignored
|
||||||
self._gcode.insert(0, "M110")
|
self._gcode.insert(0, "M110")
|
||||||
self._gcode_position = 0
|
self._gcode_position = 0
|
||||||
self._is_printing = True
|
|
||||||
self._print_start_time = time()
|
self._print_start_time = time()
|
||||||
|
|
||||||
self._print_estimated_time = int(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.Seconds))
|
self._print_estimated_time = int(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.Seconds))
|
||||||
|
@ -206,6 +208,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
||||||
for i in range(0, 4): # Push first 4 entries before accepting other inputs
|
for i in range(0, 4): # Push first 4 entries before accepting other inputs
|
||||||
self._sendNextGcodeLine()
|
self._sendNextGcodeLine()
|
||||||
|
|
||||||
|
self._is_printing = True
|
||||||
self.writeFinished.emit(self)
|
self.writeFinished.emit(self)
|
||||||
|
|
||||||
def _autoDetectFinished(self, job: AutoDetectBaudJob):
|
def _autoDetectFinished(self, job: AutoDetectBaudJob):
|
||||||
|
@ -237,7 +240,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
||||||
container_stack = Application.getInstance().getGlobalContainerStack()
|
container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
num_extruders = container_stack.getProperty("machine_extruder_count", "value")
|
num_extruders = container_stack.getProperty("machine_extruder_count", "value")
|
||||||
# Ensure that a printer is created.
|
# Ensure that a printer is created.
|
||||||
self._printers = [PrinterOutputModel(output_controller=USBPrinterOutputController(self), number_of_extruders=num_extruders)]
|
self._printers = [PrinterOutputModel(output_controller=GenericOutputController(self), number_of_extruders=num_extruders)]
|
||||||
self._printers[0].updateName(container_stack.getName())
|
self._printers[0].updateName(container_stack.getName())
|
||||||
self.setConnectionState(ConnectionState.connected)
|
self.setConnectionState(ConnectionState.connected)
|
||||||
self._update_thread.start()
|
self._update_thread.start()
|
||||||
|
@ -266,8 +269,10 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
||||||
command = (command + "\n").encode()
|
command = (command + "\n").encode()
|
||||||
if not command.endswith(b"\n"):
|
if not command.endswith(b"\n"):
|
||||||
command += b"\n"
|
command += b"\n"
|
||||||
self._serial.write(b"\n")
|
try:
|
||||||
self._serial.write(command)
|
self._serial.write(command)
|
||||||
|
except SerialTimeoutException:
|
||||||
|
Logger.log("w", "Timeout when sending command to printer via USB.")
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
while self._connection_state == ConnectionState.connected and self._serial is not None:
|
while self._connection_state == ConnectionState.connected and self._serial is not None:
|
||||||
|
@ -281,7 +286,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
||||||
self.sendCommand("M105")
|
self.sendCommand("M105")
|
||||||
self._last_temperature_request = time()
|
self._last_temperature_request = time()
|
||||||
|
|
||||||
if b"ok T:" in line or line.startswith(b"T:"): # Temperature message
|
if b"ok T:" in line or line.startswith(b"T:") or b"ok B:" in line or line.startswith(b"B:"): # Temperature message. 'T:' for extruder and 'B:' for bed
|
||||||
extruder_temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line)
|
extruder_temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line)
|
||||||
# Update all temperature values
|
# Update all temperature values
|
||||||
for match, extruder in zip(extruder_temperature_matches, self._printers[0].extruders):
|
for match, extruder in zip(extruder_temperature_matches, self._printers[0].extruders):
|
||||||
|
@ -299,6 +304,9 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
||||||
self._printers[0].updateTargetBedTemperature(float(match[1]))
|
self._printers[0].updateTargetBedTemperature(float(match[1]))
|
||||||
|
|
||||||
if self._is_printing:
|
if self._is_printing:
|
||||||
|
if line.startswith(b'!!'):
|
||||||
|
Logger.log('e', "Printer signals fatal error. Cancelling print. {}".format(line))
|
||||||
|
self.cancelPrint()
|
||||||
if b"ok" in line:
|
if b"ok" in line:
|
||||||
if not self._command_queue.empty():
|
if not self._command_queue.empty():
|
||||||
self._sendCommand(self._command_queue.get())
|
self._sendCommand(self._command_queue.get())
|
||||||
|
@ -364,7 +372,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
||||||
elapsed_time = int(time() - self._print_start_time)
|
elapsed_time = int(time() - self._print_start_time)
|
||||||
print_job = self._printers[0].activePrintJob
|
print_job = self._printers[0].activePrintJob
|
||||||
if print_job is None:
|
if print_job is None:
|
||||||
print_job = PrintJobOutputModel(output_controller = USBPrinterOutputController(self), name= Application.getInstance().getPrintInformation().jobName)
|
print_job = PrintJobOutputModel(output_controller = GenericOutputController(self), name= Application.getInstance().getPrintInformation().jobName)
|
||||||
print_job.updateState("printing")
|
print_job.updateState("printing")
|
||||||
self._printers[0].updateActivePrintJob(print_job)
|
self._printers[0].updateActivePrintJob(print_job)
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from cura.MachineAction import MachineAction
|
from cura.MachineAction import MachineAction
|
||||||
from cura.PrinterOutputDevice import PrinterOutputDevice
|
from cura.PrinterOutputDevice import PrinterOutputDevice
|
||||||
|
|
||||||
|
@ -5,6 +10,7 @@ from UM.FlameProfiler import pyqtSlot
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
from UM.Logger import Logger
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,38 +32,45 @@ class BedLevelMachineAction(MachineAction):
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def startBedLeveling(self):
|
def startBedLeveling(self):
|
||||||
self._bed_level_position = 0
|
self._bed_level_position = 0
|
||||||
printer_output_devices = self._getPrinterOutputDevices()
|
|
||||||
if printer_output_devices:
|
|
||||||
printer_output_devices[0].homeBed()
|
|
||||||
printer_output_devices[0].moveHead(0, 0, 3)
|
|
||||||
printer_output_devices[0].homeHead()
|
|
||||||
|
|
||||||
def _getPrinterOutputDevices(self):
|
printer_output_devices = self._getPrinterOutputDevices()
|
||||||
|
if not printer_output_devices:
|
||||||
|
Logger.log("e", "Can't start bed levelling. The printer connection seems to have been lost.")
|
||||||
|
return
|
||||||
|
printer = printer_output_devices[0].activePrinter
|
||||||
|
|
||||||
|
printer.homeBed()
|
||||||
|
printer.moveHead(0, 0, 3)
|
||||||
|
printer.homeHead()
|
||||||
|
|
||||||
|
def _getPrinterOutputDevices(self) -> List[PrinterOutputDevice]:
|
||||||
return [printer_output_device for printer_output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices() if isinstance(printer_output_device, PrinterOutputDevice)]
|
return [printer_output_device for printer_output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices() if isinstance(printer_output_device, PrinterOutputDevice)]
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def moveToNextLevelPosition(self):
|
def moveToNextLevelPosition(self):
|
||||||
output_devices = self._getPrinterOutputDevices()
|
output_devices = self._getPrinterOutputDevices()
|
||||||
if output_devices: # We found at least one output device
|
if not output_devices: #No output devices. Can't move.
|
||||||
output_device = output_devices[0]
|
Logger.log("e", "Can't move to the next position. The printer connection seems to have been lost.")
|
||||||
|
return
|
||||||
|
printer = output_devices[0].activePrinter
|
||||||
|
|
||||||
if self._bed_level_position == 0:
|
if self._bed_level_position == 0:
|
||||||
output_device.moveHead(0, 0, 3)
|
printer.moveHead(0, 0, 3)
|
||||||
output_device.homeHead()
|
printer.homeHead()
|
||||||
output_device.moveHead(0, 0, 3)
|
printer.moveHead(0, 0, 3)
|
||||||
output_device.moveHead(Application.getInstance().getGlobalContainerStack().getProperty("machine_width", "value") - 10, 0, 0)
|
printer.moveHead(Application.getInstance().getGlobalContainerStack().getProperty("machine_width", "value") - 10, 0, 0)
|
||||||
output_device.moveHead(0, 0, -3)
|
printer.moveHead(0, 0, -3)
|
||||||
self._bed_level_position += 1
|
self._bed_level_position += 1
|
||||||
elif self._bed_level_position == 1:
|
elif self._bed_level_position == 1:
|
||||||
output_device.moveHead(0, 0, 3)
|
printer.moveHead(0, 0, 3)
|
||||||
output_device.moveHead(-Application.getInstance().getGlobalContainerStack().getProperty("machine_width", "value" ) / 2, Application.getInstance().getGlobalContainerStack().getProperty("machine_depth", "value") - 10, 0)
|
printer.moveHead(-Application.getInstance().getGlobalContainerStack().getProperty("machine_width", "value" ) / 2, Application.getInstance().getGlobalContainerStack().getProperty("machine_depth", "value") - 10, 0)
|
||||||
output_device.moveHead(0, 0, -3)
|
printer.moveHead(0, 0, -3)
|
||||||
self._bed_level_position += 1
|
self._bed_level_position += 1
|
||||||
elif self._bed_level_position == 2:
|
elif self._bed_level_position == 2:
|
||||||
output_device.moveHead(0, 0, 3)
|
printer.moveHead(0, 0, 3)
|
||||||
output_device.moveHead(-Application.getInstance().getGlobalContainerStack().getProperty("machine_width", "value") / 2 + 10, -(Application.getInstance().getGlobalContainerStack().getProperty("machine_depth", "value") + 10), 0)
|
printer.moveHead(-Application.getInstance().getGlobalContainerStack().getProperty("machine_width", "value") / 2 + 10, -(Application.getInstance().getGlobalContainerStack().getProperty("machine_depth", "value") + 10), 0)
|
||||||
output_device.moveHead(0, 0, -3)
|
printer.moveHead(0, 0, -3)
|
||||||
self._bed_level_position += 1
|
self._bed_level_position += 1
|
||||||
elif self._bed_level_position >= 3:
|
elif self._bed_level_position >= 3:
|
||||||
output_device.sendCommand("M18") # Turn off all motors so the user can move the axes
|
output_devices[0].sendCommand("M18") # Turn off all motors so the user can move the axes
|
||||||
self.setFinished()
|
self.setFinished()
|
|
@ -153,6 +153,10 @@ class VersionUpgrade26to27(VersionUpgrade):
|
||||||
if new_id is not None:
|
if new_id is not None:
|
||||||
parser.set("containers", key, new_id)
|
parser.set("containers", key, new_id)
|
||||||
|
|
||||||
|
if "6" not in parser["containers"]:
|
||||||
|
parser["containers"]["6"] = parser["containers"]["5"]
|
||||||
|
parser["containers"]["5"] = "empty"
|
||||||
|
|
||||||
for each_section in ("general", "metadata"):
|
for each_section in ("general", "metadata"):
|
||||||
if not parser.has_section(each_section):
|
if not parser.has_section(each_section):
|
||||||
parser.add_section(each_section)
|
parser.add_section(each_section)
|
||||||
|
|
|
@ -33,6 +33,10 @@ def getMetaData():
|
||||||
"get_version": upgrade.getCfgVersion,
|
"get_version": upgrade.getCfgVersion,
|
||||||
"location": {"./extruders"}
|
"location": {"./extruders"}
|
||||||
},
|
},
|
||||||
|
"quality": {
|
||||||
|
"get_version": upgrade.getCfgVersion,
|
||||||
|
"location": {"./quality"}
|
||||||
|
},
|
||||||
"quality_changes": {
|
"quality_changes": {
|
||||||
"get_version": upgrade.getCfgVersion,
|
"get_version": upgrade.getCfgVersion,
|
||||||
"location": {"./quality"}
|
"location": {"./quality"}
|
||||||
|
|
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