Merge branch 'master' into python_type_hinting

This commit is contained in:
Simon Edwards 2016-12-13 14:39:35 +01:00
commit d4619da358
132 changed files with 42584 additions and 952 deletions

View file

@ -11,6 +11,7 @@ from UM.Application import Application
from UM.Resources import Resources
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Math.Vector import Vector
from UM.Math.Matrix import Matrix
from UM.Math.Color import Color
from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Polygon import Polygon
@ -23,6 +24,7 @@ catalog = i18nCatalog("cura")
import numpy
import copy
import math
# Setting for clearance around the prime
PRIME_CLEARANCE = 6.5
@ -43,6 +45,7 @@ class BuildVolume(SceneNode):
self._width = 0
self._height = 0
self._depth = 0
self._shape = ""
self._shader = None
@ -137,6 +140,9 @@ class BuildVolume(SceneNode):
def setDepth(self, depth):
if depth: self._depth = depth
def setShape(self, shape):
if shape: self._shape = shape
def getDisallowedAreas(self):
return self._disallowed_areas
@ -175,27 +181,70 @@ class BuildVolume(SceneNode):
min_d = -self._depth / 2
max_d = self._depth / 2
mb = MeshBuilder()
z_fight_distance = 0.2 # Distance between buildplate and disallowed area meshes to prevent z-fighting
# Outline 'cube' of the build volume
mb.addLine(Vector(min_w, min_h, min_d), Vector(max_w, min_h, min_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, max_h, min_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(min_w, max_h, min_d), Vector(max_w, max_h, min_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, max_h, min_d), color = self.VolumeOutlineColor)
if self._shape != "elliptic":
# Outline 'cube' of the build volume
mb = MeshBuilder()
mb.addLine(Vector(min_w, min_h, min_d), Vector(max_w, min_h, min_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, max_h, min_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(min_w, max_h, min_d), Vector(max_w, max_h, min_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, max_h, min_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(min_w, min_h, max_d), Vector(max_w, min_h, max_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(min_w, min_h, max_d), Vector(min_w, max_h, max_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(min_w, max_h, max_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(max_w, min_h, max_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(min_w, min_h, max_d), Vector(max_w, min_h, max_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(min_w, min_h, max_d), Vector(min_w, max_h, max_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(min_w, max_h, max_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(max_w, min_h, max_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, min_h, max_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, min_h, max_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, min_h, max_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, min_h, max_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color = self.VolumeOutlineColor)
mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor)
self.setMeshData(mb.build())
self.setMeshData(mb.build())
mb = MeshBuilder()
# Build plate grid mesh
mb = MeshBuilder()
mb.addQuad(
Vector(min_w, min_h - z_fight_distance, min_d),
Vector(max_w, min_h - z_fight_distance, min_d),
Vector(max_w, min_h - z_fight_distance, max_d),
Vector(min_w, min_h - z_fight_distance, max_d)
)
for n in range(0, 6):
v = mb.getVertex(n)
mb.setVertexUVCoordinates(n, v[0], v[2])
self._grid_mesh = mb.build()
else:
# Bottom and top 'ellipse' of the build volume
aspect = 1.0
scale_matrix = Matrix()
if self._width != 0:
# Scale circular meshes by aspect ratio if width != height
aspect = self._height / self._width
scale_matrix.compose(scale = Vector(1, 1, aspect))
mb = MeshBuilder()
mb.addArc(max_w, Vector.Unit_Y, center = (0, min_h - z_fight_distance, 0), color = self.VolumeOutlineColor)
mb.addArc(max_w, Vector.Unit_Y, center = (0, max_h, 0), color = self.VolumeOutlineColor)
self.setMeshData(mb.build().getTransformed(scale_matrix))
# Build plate grid mesh
mb = MeshBuilder()
mb.addVertex(0, min_h - z_fight_distance, 0)
mb.addArc(max_w, Vector.Unit_Y, center = Vector(0, min_h - z_fight_distance, 0))
sections = mb.getVertexCount() - 1 # Center point is not an arc section
indices = []
for n in range(0, sections - 1):
indices.append([0, n + 2, n + 1])
mb.addIndices(numpy.asarray(indices, dtype = numpy.int32))
mb.calculateNormals()
for n in range(0, mb.getVertexCount()):
v = mb.getVertex(n)
mb.setVertexUVCoordinates(n, v[0], v[2] * aspect)
self._grid_mesh = mb.build().getTransformed(scale_matrix)
# Indication of the machine origin
if self._global_container_stack.getProperty("machine_center_is_zero", "value"):
@ -203,6 +252,7 @@ class BuildVolume(SceneNode):
else:
origin = Vector(min_w, min_h, max_d)
mb = MeshBuilder()
mb.addCube(
width = self._origin_line_length,
height = self._origin_line_width,
@ -226,19 +276,6 @@ class BuildVolume(SceneNode):
)
self._origin_mesh = mb.build()
mb = MeshBuilder()
mb.addQuad(
Vector(min_w, min_h - 0.2, min_d),
Vector(max_w, min_h - 0.2, min_d),
Vector(max_w, min_h - 0.2, max_d),
Vector(min_w, min_h - 0.2, max_d)
)
for n in range(0, 6):
v = mb.getVertex(n)
mb.setVertexUVCoordinates(n, v[0], v[2])
self._grid_mesh = mb.build()
disallowed_area_height = 0.1
disallowed_area_size = 0
if self._disallowed_areas:
@ -351,6 +388,7 @@ class BuildVolume(SceneNode):
self._height = self._global_container_stack.getProperty("machine_height", "value")
self._build_volume_message.hide()
self._depth = self._global_container_stack.getProperty("machine_depth", "value")
self._shape = self._global_container_stack.getProperty("machine_shape", "value")
self._updateDisallowedAreas()
self._updateRaftThickness()
@ -410,6 +448,13 @@ class BuildVolume(SceneNode):
used_extruders = extruder_manager.getUsedExtruderStacks()
disallowed_border_size = self._getEdgeDisallowedSize()
if not used_extruders:
# If no extruder is used, assume that the active extruder is used (else nothing is drawn)
if extruder_manager.getActiveExtruderStack():
used_extruders = [extruder_manager.getActiveExtruderStack()]
else:
used_extruders = [self._global_container_stack]
result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) #Normal machine disallowed areas can always be added.
prime_areas = self._computeDisallowedAreasPrime(disallowed_border_size, used_extruders)
prime_disallowed_areas = self._computeDisallowedAreasStatic(0, used_extruders) #Where the priming is not allowed to happen. This is not added to the result, just for collision checking.
@ -440,10 +485,7 @@ class BuildVolume(SceneNode):
if collision:
break
if not collision:
#Prime areas are valid. Add as normal.
result_areas[extruder_id].extend(prime_areas[extruder_id])
result_areas[extruder_id].extend(prime_areas[extruder_id])
nozzle_disallowed_areas = extruder.getProperty("nozzle_disallowed_areas", "value")
for area in nozzle_disallowed_areas:
@ -582,34 +624,79 @@ class BuildVolume(SceneNode):
bottom_unreachable_border = max(bottom_unreachable_border, other_offset_y - offset_y)
half_machine_width = self._global_container_stack.getProperty("machine_width", "value") / 2
half_machine_depth = self._global_container_stack.getProperty("machine_depth", "value") / 2
if border_size - left_unreachable_border > 0:
result[extruder_id].append(Polygon(numpy.array([
[-half_machine_width, -half_machine_depth],
[-half_machine_width, half_machine_depth],
[-half_machine_width + border_size - left_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border],
[-half_machine_width + border_size - left_unreachable_border, -half_machine_depth + border_size - top_unreachable_border]
], numpy.float32)))
if border_size + right_unreachable_border > 0:
result[extruder_id].append(Polygon(numpy.array([
[half_machine_width, half_machine_depth],
[half_machine_width, -half_machine_depth],
[half_machine_width - border_size - right_unreachable_border, -half_machine_depth + border_size - top_unreachable_border],
[half_machine_width - border_size - right_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border]
], numpy.float32)))
if border_size + bottom_unreachable_border > 0:
result[extruder_id].append(Polygon(numpy.array([
[-half_machine_width, half_machine_depth],
[half_machine_width, half_machine_depth],
[half_machine_width - border_size - right_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border],
[-half_machine_width + border_size - left_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border]
], numpy.float32)))
if border_size - top_unreachable_border > 0:
result[extruder_id].append(Polygon(numpy.array([
[half_machine_width, -half_machine_depth],
[-half_machine_width, -half_machine_depth],
[-half_machine_width + border_size - left_unreachable_border, -half_machine_depth + border_size - top_unreachable_border],
[half_machine_width - border_size - right_unreachable_border, -half_machine_depth + border_size - top_unreachable_border]
], numpy.float32)))
if self._shape != "elliptic":
if border_size - left_unreachable_border > 0:
result[extruder_id].append(Polygon(numpy.array([
[-half_machine_width, -half_machine_depth],
[-half_machine_width, half_machine_depth],
[-half_machine_width + border_size - left_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border],
[-half_machine_width + border_size - left_unreachable_border, -half_machine_depth + border_size - top_unreachable_border]
], numpy.float32)))
if border_size + right_unreachable_border > 0:
result[extruder_id].append(Polygon(numpy.array([
[half_machine_width, half_machine_depth],
[half_machine_width, -half_machine_depth],
[half_machine_width - border_size - right_unreachable_border, -half_machine_depth + border_size - top_unreachable_border],
[half_machine_width - border_size - right_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border]
], numpy.float32)))
if border_size + bottom_unreachable_border > 0:
result[extruder_id].append(Polygon(numpy.array([
[-half_machine_width, half_machine_depth],
[half_machine_width, half_machine_depth],
[half_machine_width - border_size - right_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border],
[-half_machine_width + border_size - left_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border]
], numpy.float32)))
if border_size - top_unreachable_border > 0:
result[extruder_id].append(Polygon(numpy.array([
[half_machine_width, -half_machine_depth],
[-half_machine_width, -half_machine_depth],
[-half_machine_width + border_size - left_unreachable_border, -half_machine_depth + border_size - top_unreachable_border],
[half_machine_width - border_size - right_unreachable_border, -half_machine_depth + border_size - top_unreachable_border]
], numpy.float32)))
else:
sections = 32
arc_vertex = [0, half_machine_depth - border_size]
for i in range(0, sections):
quadrant = math.floor(4 * i / sections)
vertices = []
if quadrant == 0:
vertices.append([-half_machine_width, half_machine_depth])
elif quadrant == 1:
vertices.append([-half_machine_width, -half_machine_depth])
elif quadrant == 2:
vertices.append([half_machine_width, -half_machine_depth])
elif quadrant == 3:
vertices.append([half_machine_width, half_machine_depth])
vertices.append(arc_vertex)
angle = 2 * math.pi * (i + 1) / sections
arc_vertex = [-(half_machine_width - border_size) * math.sin(angle), (half_machine_depth - border_size) * math.cos(angle)]
vertices.append(arc_vertex)
result[extruder_id].append(Polygon(numpy.array(vertices, numpy.float32)))
if border_size > 0:
result[extruder_id].append(Polygon(numpy.array([
[-half_machine_width, -half_machine_depth],
[-half_machine_width, half_machine_depth],
[-half_machine_width + border_size, 0]
], numpy.float32)))
result[extruder_id].append(Polygon(numpy.array([
[-half_machine_width, half_machine_depth],
[ half_machine_width, half_machine_depth],
[ 0, half_machine_depth - border_size]
], numpy.float32)))
result[extruder_id].append(Polygon(numpy.array([
[ half_machine_width, half_machine_depth],
[ half_machine_width, -half_machine_depth],
[ half_machine_width - border_size, 0]
], numpy.float32)))
result[extruder_id].append(Polygon(numpy.array([
[ half_machine_width,-half_machine_depth],
[-half_machine_width,-half_machine_depth],
[ 0, -half_machine_depth + border_size]
], numpy.float32)))
return result
@ -683,7 +770,7 @@ class BuildVolume(SceneNode):
skirt_distance = self._getSettingFromAdhesionExtruder("skirt_gap")
skirt_line_count = self._getSettingFromAdhesionExtruder("skirt_line_count")
bed_adhesion_size = skirt_distance + (skirt_line_count * self._getSettingFromAdhesionExtruder("skirt_brim_line_width"))
if self._global_container_stack.getProperty("machine_extruder_count", "value") > 1:
if len(ExtruderManager.getInstance().getUsedExtruderStacks()) > 1:
adhesion_extruder_nr = int(self._global_container_stack.getProperty("adhesion_extruder_nr", "value"))
extruder_values = ExtruderManager.getInstance().getAllExtruderValues("skirt_brim_line_width")
del extruder_values[adhesion_extruder_nr] # Remove the value of the adhesion extruder nr.
@ -716,15 +803,16 @@ class BuildVolume(SceneNode):
move_from_wall_radius = 0 # Moves that start from outer wall.
move_from_wall_radius = max(move_from_wall_radius, max(self._getSettingFromAllExtruders("infill_wipe_dist")))
avoid_enabled_per_extruder = self._getSettingFromAllExtruders(("travel_avoid_other_parts"))
avoid_distance_per_extruder = self._getSettingFromAllExtruders("travel_avoid_distance")
for index, avoid_other_parts_enabled in enumerate(avoid_enabled_per_extruder): #For each extruder (or just global).
used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks()
avoid_enabled_per_extruder = [stack.getProperty("travel_avoid_other_parts","value") for stack in used_extruders]
travel_avoid_distance_per_extruder = [stack.getProperty("travel_avoid_distance", "value") for stack in used_extruders]
for avoid_other_parts_enabled, avoid_distance in zip(avoid_enabled_per_extruder, travel_avoid_distance_per_extruder): #For each extruder (or just global).
if avoid_other_parts_enabled:
move_from_wall_radius = max(move_from_wall_radius, avoid_distance_per_extruder[index]) #Index of the same extruder.
move_from_wall_radius = max(move_from_wall_radius, avoid_distance)
#Now combine our different pieces of data to get the final border size.
#Support expansion is added to the bed adhesion, since the bed adhesion goes around support.
#Support expansion is added to farthest shield distance, since the shields go around support.
# Now combine our different pieces of data to get the final border size.
# Support expansion is added to the bed adhesion, since the bed adhesion goes around support.
# Support expansion is added to farthest shield distance, since the shields go around support.
border_size = max(move_from_wall_radius, support_expansion + farthest_shield_distance, support_expansion + bed_adhesion_size)
return border_size

View file

@ -2,9 +2,11 @@ import sys
import platform
import traceback
import webbrowser
import urllib
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QCoreApplication
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, Qt, QCoreApplication
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QHBoxLayout, QVBoxLayout, QLabel, QTextEdit
from UM.Logger import Logger
from UM.i18n import i18nCatalog
@ -22,9 +24,7 @@ fatal_exception_types = [
]
def show(exception_type, value, tb):
debug_mode = False
if QCoreApplication.instance():
debug_mode = QCoreApplication.instance().getCommandLineOption("debug-mode", False)
debug_mode = True
Logger.log("c", "An uncaught exception has occurred!")
for line in traceback.format_exception(exception_type, value, tb):
@ -38,14 +38,41 @@ def show(exception_type, value, tb):
if not application:
sys.exit(1)
dialog = QDialog()
dialog.setMinimumWidth(640)
dialog.setMinimumHeight(640)
dialog.setWindowTitle(catalog.i18nc("@title:window", "Oops!"))
layout = QVBoxLayout(dialog)
label = QLabel(dialog)
pixmap = QPixmap()
try:
data = urllib.request.urlopen("http://www.randomkittengenerator.com/cats/rotator.php").read()
pixmap.loadFromData(data)
except:
try:
from UM.Resources import Resources
path = Resources.getPath(Resources.Images, "kitten.jpg")
pixmap.load(path)
except:
pass
pixmap = pixmap.scaled(150, 150)
label.setPixmap(pixmap)
label.setAlignment(Qt.AlignCenter)
layout.addWidget(label)
label.setText(catalog.i18nc("@label", "<p>A fatal exception has occurred that we could not recover from!</p><p>Please use the information below to post a bug report at <a href=\"http://github.com/Ultimaker/Cura/issues\">http://github.com/Ultimaker/Cura/issues</a></p>"))
label = QLabel(dialog)
layout.addWidget(label)
#label.setScaledContents(True)
label.setText(catalog.i18nc("@label", """<p>A fatal exception has occurred that we could not recover from!</p>
<p>We hope this picture of a kitten helps you recover from the shock.</p>
<p>Please use the information below to post a bug report at <a href=\"http://github.com/Ultimaker/Cura/issues\">http://github.com/Ultimaker/Cura/issues</a></p>
"""))
textarea = QTextEdit(dialog)
layout.addWidget(textarea)

View file

@ -7,6 +7,7 @@ from UM.Scene.Camera import Camera
from UM.Math.Vector import Vector
from UM.Math.Quaternion import Quaternion
from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Matrix import Matrix
from UM.Resources import Resources
from UM.Scene.ToolHandle import ToolHandle
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
@ -61,43 +62,12 @@ from PyQt5.QtGui import QColor, QIcon
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
from contextlib import contextmanager
import sys
import os.path
import numpy
import copy
import urllib
import urllib.parse
import os
import time
CONFIG_LOCK_FILENAME = "cura.lock"
## Contextmanager to create a lock file and remove it afterwards.
@contextmanager
def lockFile(filename):
try:
with open(filename, 'w') as lock_file:
lock_file.write("Lock file - Cura is currently writing")
except:
Logger.log("e", "Could not create lock file [%s]" % filename)
yield
try:
if os.path.exists(filename):
os.remove(filename)
except:
Logger.log("e", "Could not delete lock file [%s]" % filename)
## Wait for a lock file to disappear
# the maximum allowable age is settable; if the file is too old, it will be ignored too
def waitFileDisappear(filename, max_age_seconds=10, msg=""):
now = time.time()
while os.path.exists(filename) and now < os.path.getmtime(filename) + max_age_seconds and now > os.path.getmtime(filename):
if msg:
Logger.log("d", msg)
time.sleep(1)
now = time.time()
numpy.seterr(all="ignore")
@ -145,7 +115,7 @@ class CuraApplication(QtApplication):
# For settings which are not settable_per_mesh and not settable_per_extruder:
# A function which determines the glabel/meshgroup value by looking at the values of the setting in all (used) extruders
SettingDefinition.addSupportedProperty("resolve", DefinitionPropertyType.Function, default = None)
SettingDefinition.addSupportedProperty("resolve", DefinitionPropertyType.Function, default = None, depends_on = "value")
SettingDefinition.addSettingType("extruder", None, str, Validator)
@ -175,6 +145,7 @@ class CuraApplication(QtApplication):
{
("quality", UM.Settings.InstanceContainer.InstanceContainer.Version): (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"),
("machine_stack", UM.Settings.ContainerStack.ContainerStack.Version): (self.ResourceTypes.MachineStack, "application/x-uranium-containerstack"),
("extruder_train", UM.Settings.ContainerStack.ContainerStack.Version): (self.ResourceTypes.ExtruderStack, "application/x-uranium-extruderstack"),
("preferences", Preferences.Version): (Resources.Preferences, "application/x-uranium-preferences"),
("user", UM.Settings.InstanceContainer.InstanceContainer.Version): (self.ResourceTypes.UserInstanceContainer, "application/x-uranium-instancecontainer")
}
@ -250,10 +221,8 @@ class CuraApplication(QtApplication):
empty_quality_changes_container.addMetaDataEntry("type", "quality_changes")
ContainerRegistry.getInstance().addContainer(empty_quality_changes_container)
# Set the filename to create if cura is writing in the config dir.
self._config_lock_filename = os.path.join(Resources.getConfigStoragePath(), CONFIG_LOCK_FILENAME)
self.waitConfigLockFile()
ContainerRegistry.getInstance().load()
with ContainerRegistry.getInstance().lockFile():
ContainerRegistry.getInstance().load()
Preferences.getInstance().addPreference("cura/active_mode", "simple")
Preferences.getInstance().addPreference("cura/recent_files", "")
@ -262,7 +231,8 @@ class CuraApplication(QtApplication):
Preferences.getInstance().addPreference("view/center_on_select", True)
Preferences.getInstance().addPreference("mesh/scale_to_fit", True)
Preferences.getInstance().addPreference("mesh/scale_tiny_meshes", True)
Preferences.getInstance().addPreference("cura/dialog_on_project_save", True)
Preferences.getInstance().addPreference("cura/asked_dialog_on_project_save", False)
for key in [
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
"dialog_profile_path",
@ -279,6 +249,8 @@ class CuraApplication(QtApplication):
shell
wall_thickness
top_bottom_thickness
z_seam_x
z_seam_y
infill
infill_sparse_density
material
@ -333,14 +305,6 @@ class CuraApplication(QtApplication):
self._recent_files.append(QUrl.fromLocalFile(f))
def getContainerRegistry(self):
return CuraContainerRegistry.getInstance()
## Lock file check: if (another) Cura is writing in the Config dir.
# one may not be able to read a valid set of files while writing. Not entirely fool-proof,
# but works when you start Cura shortly after shutting down.
def waitConfigLockFile(self):
waitFileDisappear(self._config_lock_filename, max_age_seconds=10, msg="Waiting for Cura to finish writing in the config dir...")
def _onEngineCreated(self):
self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
@ -368,11 +332,8 @@ class CuraApplication(QtApplication):
if not self._started: # Do not do saving during application start
return
self.waitConfigLockFile()
# When starting Cura, we check for the lockFile which is created and deleted here
with lockFile(self._config_lock_filename):
# Lock file for "more" atomically loading and saving to/from config dir.
with ContainerRegistry.getInstance().lockFile():
for instance in ContainerRegistry.getInstance().findInstanceContainers():
if not instance.isDirty():
continue
@ -397,6 +358,8 @@ class CuraApplication(QtApplication):
path = Resources.getStoragePath(self.ResourceTypes.UserInstanceContainer, file_name)
elif instance_type == "variant":
path = Resources.getStoragePath(self.ResourceTypes.VariantInstanceContainer, file_name)
elif instance_type == "definition_changes":
path = Resources.getStoragePath(self.ResourceTypes.MachineStack, file_name)
if path:
instance.setPath(path)
@ -461,7 +424,6 @@ class CuraApplication(QtApplication):
def addCommandLineOptions(self, parser):
super().addCommandLineOptions(parser)
parser.add_argument("file", nargs="*", help="Files to load after starting the application.")
parser.add_argument("--debug", dest="debug-mode", action="store_true", default=False, help="Enable detailed crash reports.")
def run(self):
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))
@ -651,7 +613,7 @@ class CuraApplication(QtApplication):
if not scene_bounding_box:
scene_bounding_box = AxisAlignedBox.Null
if repr(self._scene_bounding_box) != repr(scene_bounding_box):
if repr(self._scene_bounding_box) != repr(scene_bounding_box) and scene_bounding_box.isValid():
self._scene_bounding_box = scene_bounding_box
self.sceneBoundingBoxChanged.emit()
@ -753,6 +715,8 @@ class CuraApplication(QtApplication):
continue # Node that doesnt have a mesh and is not a group.
if node.getParent() and node.getParent().callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.isSelectable():
continue # i.e. node with layer data
Selection.add(node)
## Delete all nodes containing mesh data in the scene.
@ -792,6 +756,8 @@ class CuraApplication(QtApplication):
continue # Node that doesnt have a mesh and is not a group.
if node.getParent() and node.getParent().callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.isSelectable():
continue # i.e. node with layer data
nodes.append(node)
if nodes:
@ -818,6 +784,8 @@ class CuraApplication(QtApplication):
continue # Node that doesnt have a mesh and is not a group.
if node.getParent() and node.getParent().callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.isSelectable():
continue # i.e. node with layer data
nodes.append(node)
if nodes:
@ -901,10 +869,13 @@ class CuraApplication(QtApplication):
# 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()):
orientation = node.getOrientation().toMatrix()
rotated_mesh = mesh.getTransformed(orientation)
center = rotated_mesh.getCenterPosition().scale(node.getScale())
transformation = node.getLocalTransformation()
transformation.setTranslation(zero_translation)
transformed_mesh = mesh.getTransformed(transformation)
center = transformed_mesh.getCenterPosition()
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)
@ -916,12 +887,13 @@ class CuraApplication(QtApplication):
# Move each node to the same position.
for mesh, node in zip(meshes, group_node.getChildren()):
orientation = node.getOrientation().toMatrix()
rotated_mesh = mesh.getTransformed(orientation)
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(-rotated_mesh.getZeroPosition().scale(node.getScale()) - offset)
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)
@ -1007,7 +979,7 @@ class CuraApplication(QtApplication):
def _reloadMeshFinished(self, job):
# TODO; This needs to be fixed properly. We now make the assumption that we only load a single mesh!
mesh_data = job.getResult().getMeshData()
mesh_data = job.getResult()[0].getMeshData()
if mesh_data:
job._node.setMeshData(mesh_data)
else:

View file

@ -6,13 +6,21 @@ from PyQt5.QtQml import QQmlComponent, QQmlContext
from UM.PluginObject import PluginObject
from UM.PluginRegistry import PluginRegistry
from UM.Logger import Logger
from UM.Application import Application
import os
## Machine actions are actions that are added to a specific machine type. Examples of such actions are
# updating the firmware, connecting with remote devices or doing bed leveling. A machine action can also have a
# qml, which should contain a "Cura.MachineAction" item. When activated, the item will be displayed in a dialog
# and this object will be added as "manager" (so all pyqtSlot() functions can be called by calling manager.func())
class MachineAction(QObject, PluginObject):
## Create a new Machine action.
# \param key unique key of the machine action
# \param label Human readable label used to identify the machine action.
def __init__(self, key, label = ""):
super().__init__()
self._key = key
@ -63,13 +71,16 @@ class MachineAction(QObject, PluginObject):
def finished(self):
return self._finished
## Protected helper to create a view object based on provided QML.
def _createViewFromQML(self):
path = QUrl.fromLocalFile(
os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), self._qml_url))
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), self._qml_url))
self._component = QQmlComponent(Application.getInstance()._engine, path)
self._context = QQmlContext(Application.getInstance()._engine.rootContext())
self._context.setContextProperty("manager", self)
self._view = self._component.create(self._context)
if self._view is None:
Logger.log("c", "QQmlComponent status %s", self._component.status())
Logger.log("c", "QQmlComponent error string %s", self._component.errorString())
@pyqtProperty(QObject, constant = True)
def displayItem(self):

View file

@ -49,7 +49,9 @@ class PrinterOutputDevice(QObject, OutputDevice):
self._printer_state = ""
self._printer_type = "unknown"
def requestWrite(self, nodes, file_name = None, filter_by_machine = False):
self._camera_active = False
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None):
raise NotImplementedError("requestWrite needs to be implemented")
## Signals
@ -136,6 +138,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
@pyqtSlot()
def startCamera(self):
self._camera_active = True
self._startCamera()
def _startCamera(self):
@ -143,6 +146,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
@pyqtSlot()
def stopCamera(self):
self._camera_active = False
self._stopCamera()
def _stopCamera(self):

View file

@ -155,6 +155,18 @@ class ExtruderManager(QObject):
if changed:
self.extrudersChanged.emit(machine_id)
def registerExtruder(self, extruder_train, machine_id):
changed = False
if machine_id not in self._extruder_trains:
self._extruder_trains[machine_id] = {}
changed = True
if extruder_train:
self._extruder_trains[machine_id][extruder_train.getMetaDataEntry("position")] = extruder_train
changed = True
if changed:
self.extrudersChanged.emit(machine_id)
## Creates a container stack for an extruder train.
#
# The container stack has an extruder definition at the bottom, which is

View file

@ -141,8 +141,6 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
for extruder in manager.getMachineExtruders(global_container_stack.getId()):
extruder_name = extruder.getName()
material = extruder.findContainer({ "type": "material" })
if material and not self._simple_names:
extruder_name = "%s (%s)" % (material.getName(), extruder_name)
position = extruder.getMetaDataEntry("position", default = "0") # Get the position
try:
position = int(position)

View file

@ -13,6 +13,7 @@ from UM.Message import Message
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.SettingDefinition import SettingDefinition
from UM.Settings.SettingFunction import SettingFunction
from UM.Settings.Validator import ValidatorState
@ -39,6 +40,10 @@ class MachineManager(QObject):
self.globalContainerChanged.connect(self.activeQualityChanged)
self._stacks_have_errors = None
self._empty_variant_container = ContainerRegistry.getInstance().findInstanceContainers(id="empty_variant")[0]
self._empty_material_container = ContainerRegistry.getInstance().findInstanceContainers(id="empty_material")[0]
self._empty_quality_container = ContainerRegistry.getInstance().findInstanceContainers(id="empty_quality")[0]
self._empty_quality_changes_container = ContainerRegistry.getInstance().findInstanceContainers(id="empty_quality_changes")[0]
self._onGlobalContainerChanged()
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
@ -53,11 +58,6 @@ class MachineManager(QObject):
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeStackChanged)
self.activeStackChanged.connect(self.activeStackValueChanged)
self._empty_variant_container = ContainerRegistry.getInstance().findInstanceContainers(id = "empty_variant")[0]
self._empty_material_container = ContainerRegistry.getInstance().findInstanceContainers(id = "empty_material")[0]
self._empty_quality_container = ContainerRegistry.getInstance().findInstanceContainers(id = "empty_quality")[0]
self._empty_quality_changes_container = ContainerRegistry.getInstance().findInstanceContainers(id = "empty_quality_changes")[0]
Preferences.getInstance().addPreference("cura/active_machine", "")
self._global_event_keys = set()
@ -114,7 +114,11 @@ class MachineManager(QObject):
def printerOutputDevices(self):
return self._printer_output_devices
def _onHotendIdChanged(self, index: Union[str,int], hotend_id: str) -> None:
@pyqtProperty(int, constant=True)
def totalNumberOfSettings(self):
return len(ContainerRegistry.getInstance().findDefinitionContainers(id="fdmprinter")[0].getAllKeys())
def _onHotendIdChanged(self, index: Union[str, int], hotend_id: str) -> None:
if not self._global_container_stack:
return
@ -251,6 +255,7 @@ class MachineManager(QObject):
quality = self._global_container_stack.findContainer({"type": "quality"})
quality.nameChanged.connect(self._onQualityNameChanged)
self._updateStacksHaveErrors()
## Update self._stacks_valid according to _checkStacksForErrors and emit if change.
def _updateStacksHaveErrors(self):
@ -282,7 +287,7 @@ class MachineManager(QObject):
def _onInstanceContainersChanged(self, container):
container_type = container.getMetaDataEntry("type")
if container_type == "material":
self.activeMaterialChanged.emit()
elif container_type == "variant":
@ -290,6 +295,8 @@ class MachineManager(QObject):
elif container_type == "quality":
self.activeQualityChanged.emit()
self._updateStacksHaveErrors()
def _onPropertyChanged(self, key, property_name):
if property_name == "value":
# Notify UI items, such as the "changed" star in profile pull down menu.
@ -302,6 +309,16 @@ class MachineManager(QObject):
changed_validation_state = self._active_container_stack.getProperty(key, property_name)
else:
changed_validation_state = self._global_container_stack.getProperty(key, property_name)
if changed_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 defintions value can be set by a function, which could
# be an invalid setting.
definition = self._active_container_stack.getSettingDefinition(key)
validator_type = SettingDefinition.getValidatorForType(definition.type)
if validator_type:
validator = validator_type(key)
changed_validation_state = validator(self._active_container_stack)
if changed_validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
self._stacks_have_errors = True
self.stacksValidationChanged.emit()
@ -311,6 +328,7 @@ class MachineManager(QObject):
@pyqtSlot(str)
def setActiveMachine(self, stack_id: str) -> None:
self.blurSettings.emit() # Ensure no-one has focus.
containers = ContainerRegistry.getInstance().findContainerStacks(id = stack_id)
if containers:
Application.getInstance().setGlobalContainerStack(containers[0])
@ -476,6 +494,16 @@ class MachineManager(QObject):
return ""
@pyqtProperty("QVariantList", notify = activeMaterialChanged)
def activeMaterialNames(self):
result = []
if ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks() is not None:
for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
material_container = stack.findContainer(type="material")
if material_container and material_container != self._empty_material_container:
result.append(material_container.getName())
return result
@pyqtProperty(str, notify=activeMaterialChanged)
def activeMaterialId(self) -> str:
if self._active_container_stack:
@ -867,7 +895,8 @@ class MachineManager(QObject):
if old_container:
old_container.nameChanged.disconnect(self._onQualityNameChanged)
else:
Logger.log("w", "Could not find old "+ container.getMetaDataEntry("type") + " while changing active " + container.getMetaDataEntry("type") + ".")
Logger.log("e", "Could not find container of type %s in stack %s while replacing quality (changes) with container %s", container.getMetaDataEntry("type"), stack.getId(), container.getId())
return
# Swap in the new container into the stack.
stack.replaceContainer(stack.getContainerIndex(old_container), container, postpone_emit = postpone_emit)
@ -878,7 +907,7 @@ class MachineManager(QObject):
def _askUserToKeepOrClearCurrentSettings(self):
# Ask the user if the user profile should be cleared or not (discarding the current settings)
# In Simple Mode we assume the user always wants to keep the (limited) current settings
details_text = catalog.i18nc("@label", "You made changes to the following setting(s):")
details_text = catalog.i18nc("@label", "You made changes to the following setting(s)/override(s):")
# user changes in global stack
details_list = [setting.definition.label for setting in self._global_container_stack.getTop().findInstances(**{})]
@ -893,14 +922,19 @@ class MachineManager(QObject):
# Format to output string
details = "\n ".join([details_text, ] + details_list)
Application.getInstance().messageBox(catalog.i18nc("@window:title", "Switched profiles"),
catalog.i18nc("@label",
"Do you want to transfer your changed settings to this profile?"),
catalog.i18nc("@label",
"If you transfer your settings they will override settings in the profile."),
details,
buttons=QMessageBox.Yes + QMessageBox.No, icon=QMessageBox.Question,
callback=self._keepUserSettingsDialogCallback)
num_changed_settings = len(details_list)
Application.getInstance().messageBox(
catalog.i18nc("@window:title", "Switched profiles"),
catalog.i18nc(
"@label",
"Do you want to transfer your %d changed setting(s)/override(s) to this profile?") % num_changed_settings,
catalog.i18nc(
"@label",
"If you transfer your settings they will override settings in the profile. If you don't transfer these settings, they will be lost."),
details,
buttons=QMessageBox.Yes + QMessageBox.No,
icon=QMessageBox.Question,
callback=self._keepUserSettingsDialogCallback)
def _keepUserSettingsDialogCallback(self, button):
if button == QMessageBox.Yes:
@ -1170,8 +1204,9 @@ class MachineManager(QObject):
else:
material_search_criteria["definition"] = "fdmprinter"
material_containers = container_registry.findInstanceContainers(**material_search_criteria)
if material_containers:
search_criteria["material"] = material_containers[0].getId()
# Try all materials to see if there is a quality profile available.
for material_container in material_containers:
search_criteria["material"] = material_container.getId()
containers = container_registry.findInstanceContainers(**search_criteria)
if containers:

View file

@ -8,7 +8,7 @@ class MaterialSettingsVisibilityHandler(SettingVisibilityHandler):
super().__init__(parent = parent, *args, **kwargs)
material_settings = set([
"material_print_temperature",
"default_material_print_temperature",
"material_bed_temperature",
"material_standby_temperature",
"cool_fan_speed",

View file

@ -9,6 +9,10 @@ import UM.Logger
import UM.Qt
from UM.Application import Application
from UM.Settings.ContainerRegistry import ContainerRegistry
import os
from UM.i18n import i18nCatalog
class QualitySettingsModel(UM.Qt.ListModel.ListModel):
KeyRole = Qt.UserRole + 1
@ -28,6 +32,7 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
self._extruder_definition_id = None
self._quality_id = None
self._material_id = None
self._i18n_catalog = None
self.addRoleName(self.KeyRole, "key")
self.addRoleName(self.LabelRole, "label")
@ -117,6 +122,18 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
quality_type = quality_container.getMetaDataEntry("quality_type")
definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(quality_container.getDefinition())
definition = quality_container.getDefinition()
# Check if the definition container has a translation file.
definition_suffix = ContainerRegistry.getMimeTypeForContainer(type(definition)).preferredSuffix
catalog = i18nCatalog(os.path.basename(definition_id + "." + definition_suffix))
if catalog.hasTranslationLoaded():
self._i18n_catalog = catalog
for file_name in quality_container.getDefinition().getInheritedFiles():
catalog = i18nCatalog(os.path.basename(file_name))
if catalog.hasTranslationLoaded():
self._i18n_catalog = catalog
criteria = {"type": "quality", "quality_type": quality_type, "definition": definition_id}
@ -167,6 +184,8 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
for definition in definition_container.findDefinitions():
if definition.type == "category":
current_category = definition.label
if self._i18n_catalog:
current_category = self._i18n_catalog.i18nc(definition.key + " label", definition.label)
continue
profile_value = None
@ -177,6 +196,12 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
profile_value_source = container.getMetaDataEntry("type")
profile_value = new_value
# Global tab should use resolve (if there is one)
if not self._extruder_id:
resolve_value = global_container_stack.getProperty(definition.key, "resolve")
if resolve_value is not None and profile_value is not None:
profile_value = resolve_value
user_value = None
if not self._extruder_id:
user_value = global_container_stack.getTop().getProperty(definition.key, "value")
@ -198,9 +223,14 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
if self._extruder_id == "" and settable_per_extruder:
continue
label = definition.label
if self._i18n_catalog:
label = self._i18n_catalog.i18nc(definition.key + " label", label)
items.append({
"key": definition.key,
"label": definition.label,
"label": label,
"unit": definition.unit,
"profile_value": "" if profile_value is None else str(profile_value), # it is for display only
"profile_value_source": profile_value_source,
@ -208,4 +238,4 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
"category": current_category
})
self.setItems(items)
self.setItems(items)

View file

@ -89,7 +89,7 @@ class SettingInheritanceManager(QObject):
self._update() # Ensure that the settings_with_inheritance_warning list is populated.
def _onPropertyChanged(self, key, property_name):
if property_name == "value" and self._global_container_stack:
if (property_name == "value" or property_name == "enabled") and self._global_container_stack:
definitions = self._global_container_stack.getBottom().findDefinitions(key = key)
if not definitions:
return
@ -171,7 +171,16 @@ class SettingInheritanceManager(QObject):
continue
if value is not None:
# If a setting doesn't use any keys, it won't change it's value, so treat it as if it's a fixed value
has_setting_function = isinstance(value, SettingFunction) and len(value.getUsedSettingKeys()) > 0
has_setting_function = isinstance(value, SettingFunction)
if has_setting_function:
for setting_key in value.getUsedSettingKeys():
if setting_key in self._active_container_stack.getAllKeys():
break # We found an actual setting. So has_setting_function can remain true
else:
# All of the setting_keys turned out to not be setting keys at all!
# This can happen due enum keys also being marked as settings.
has_setting_function = False
if has_setting_function is False:
has_non_function_value = True
continue