mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-06 14:37:29 -06:00
Merge branch 'master' of github.com:Ultimaker/Cura into cura_containerstack
This commit is contained in:
commit
d7004d3547
290 changed files with 90954 additions and 68206 deletions
|
@ -16,7 +16,6 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
|||
from UM.Mesh.ReadMeshJob import ReadMeshJob
|
||||
from UM.Logger import Logger
|
||||
from UM.Preferences import Preferences
|
||||
from UM.JobQueue import JobQueue
|
||||
from UM.SaveFile import SaveFile
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.Scene.GroupDecorator import GroupDecorator
|
||||
|
@ -26,15 +25,23 @@ from UM.Settings.Validator import Validator
|
|||
from UM.Message import Message
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Workspace.WorkspaceReader import WorkspaceReader
|
||||
from UM.Platform import Platform
|
||||
from UM.Decorators import deprecated
|
||||
|
||||
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
||||
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
||||
from UM.Operations.GroupedOperation import GroupedOperation
|
||||
from UM.Operations.SetTransformOperation import SetTransformOperation
|
||||
from cura.Arrange import Arrange
|
||||
from cura.ShapeArray import ShapeArray
|
||||
from cura.ConvexHullDecorator import ConvexHullDecorator
|
||||
from cura.SetParentOperation import SetParentOperation
|
||||
from cura.SliceableObjectDecorator import SliceableObjectDecorator
|
||||
from cura.BlockSlicingDecorator import BlockSlicingDecorator
|
||||
|
||||
from cura.ArrangeObjectsJob import ArrangeObjectsJob
|
||||
from cura.MultiplyObjectsJob import MultiplyObjectsJob
|
||||
|
||||
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
|
@ -90,6 +97,7 @@ if not MYPY:
|
|||
CuraVersion = "master" # [CodeStyle: Reflecting imported value]
|
||||
CuraBuildType = ""
|
||||
|
||||
|
||||
class CuraApplication(QtApplication):
|
||||
class ResourceTypes:
|
||||
QmlFiles = Resources.UserType + 1
|
||||
|
@ -104,6 +112,9 @@ class CuraApplication(QtApplication):
|
|||
Q_ENUMS(ResourceTypes)
|
||||
|
||||
def __init__(self):
|
||||
# this list of dir names will be used by UM to detect an old cura directory
|
||||
for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "user", "variants"]:
|
||||
Resources.addExpectedDirNameInData(dir_name)
|
||||
|
||||
Resources.addSearchPath(os.path.join(QtApplication.getInstallPrefix(), "share", "cura", "resources"))
|
||||
if not hasattr(sys, "frozen"):
|
||||
|
@ -184,7 +195,10 @@ class CuraApplication(QtApplication):
|
|||
"SelectionTool",
|
||||
"CameraTool",
|
||||
"GCodeWriter",
|
||||
"LocalFileOutputDevice"
|
||||
"LocalFileOutputDevice",
|
||||
"TranslateTool",
|
||||
"FileLogger",
|
||||
"XmlMaterialProfile"
|
||||
])
|
||||
self._physics = None
|
||||
self._volume = None
|
||||
|
@ -207,6 +221,7 @@ class CuraApplication(QtApplication):
|
|||
|
||||
self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity)
|
||||
self.getController().toolOperationStopped.connect(self._onToolOperationStopped)
|
||||
self.getController().contextMenuRequested.connect(self._onContextMenuRequested)
|
||||
|
||||
Resources.addType(self.ResourceTypes.QmlFiles, "qml")
|
||||
Resources.addType(self.ResourceTypes.Firmware, "firmware")
|
||||
|
@ -240,7 +255,7 @@ class CuraApplication(QtApplication):
|
|||
ContainerRegistry.getInstance().load()
|
||||
|
||||
Preferences.getInstance().addPreference("cura/active_mode", "simple")
|
||||
Preferences.getInstance().addPreference("cura/recent_files", "")
|
||||
|
||||
Preferences.getInstance().addPreference("cura/categories_expanded", "")
|
||||
Preferences.getInstance().addPreference("cura/jobname_prefix", True)
|
||||
Preferences.getInstance().addPreference("view/center_on_select", False)
|
||||
|
@ -248,11 +263,14 @@ class CuraApplication(QtApplication):
|
|||
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)
|
||||
Preferences.getInstance().addPreference("cura/choice_on_profile_override", 0)
|
||||
Preferences.getInstance().addPreference("cura/choice_on_profile_override", "always_ask")
|
||||
Preferences.getInstance().addPreference("cura/choice_on_open_project", "always_ask")
|
||||
|
||||
Preferences.getInstance().addPreference("cura/currency", "€")
|
||||
Preferences.getInstance().addPreference("cura/material_settings", "{}")
|
||||
|
||||
Preferences.getInstance().addPreference("view/invert_zoom", False)
|
||||
|
||||
for key in [
|
||||
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
|
||||
"dialog_profile_path",
|
||||
|
@ -313,20 +331,11 @@ class CuraApplication(QtApplication):
|
|||
experimental
|
||||
""".replace("\n", ";").replace(" ", ""))
|
||||
|
||||
JobQueue.getInstance().jobFinished.connect(self._onJobFinished)
|
||||
|
||||
self.applicationShuttingDown.connect(self.saveSettings)
|
||||
self.engineCreatedSignal.connect(self._onEngineCreated)
|
||||
|
||||
self.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||
self._onGlobalContainerChanged()
|
||||
self._recent_files = []
|
||||
files = Preferences.getInstance().getValue("cura/recent_files").split(";")
|
||||
for f in files:
|
||||
if not os.path.isfile(f):
|
||||
continue
|
||||
|
||||
self._recent_files.append(QUrl.fromLocalFile(f))
|
||||
|
||||
def _onEngineCreated(self):
|
||||
self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
|
||||
|
@ -344,10 +353,10 @@ class CuraApplication(QtApplication):
|
|||
|
||||
def discardOrKeepProfileChanges(self):
|
||||
choice = Preferences.getInstance().getValue("cura/choice_on_profile_override")
|
||||
if choice == 1:
|
||||
if choice == "always_discard":
|
||||
# don't show dialog and DISCARD the profile
|
||||
self.discardOrKeepProfileChangesClosed("discard")
|
||||
elif choice == 2:
|
||||
elif choice == "always_keep":
|
||||
# don't show dialog and KEEP the profile
|
||||
self.discardOrKeepProfileChangesClosed("keep")
|
||||
else:
|
||||
|
@ -593,6 +602,9 @@ class CuraApplication(QtApplication):
|
|||
# The platform is a child of BuildVolume
|
||||
self._volume = BuildVolume.BuildVolume(root)
|
||||
|
||||
# Set the build volume of the arranger to the used build volume
|
||||
Arrange.build_volume = self._volume
|
||||
|
||||
self.getRenderer().setBackgroundColor(QColor(245, 245, 245))
|
||||
|
||||
self._physics = PlatformPhysics.PlatformPhysics(controller, self._volume)
|
||||
|
@ -667,6 +679,7 @@ class CuraApplication(QtApplication):
|
|||
#
|
||||
# \param engine The QML engine.
|
||||
def registerObjects(self, engine):
|
||||
super().registerObjects(engine)
|
||||
engine.rootContext().setContextProperty("Printer", self)
|
||||
engine.rootContext().setContextProperty("CuraApplication", self)
|
||||
self._print_information = PrintInformation.PrintInformation()
|
||||
|
@ -700,14 +713,11 @@ class CuraApplication(QtApplication):
|
|||
if type_name in ("Cura", "Actions"):
|
||||
continue
|
||||
|
||||
qmlRegisterType(QUrl.fromLocalFile(path), "Cura", 1, 0, type_name)
|
||||
# Ignore anything that is not a QML file.
|
||||
if not path.endswith(".qml"):
|
||||
continue
|
||||
|
||||
## Get the backend of the application (the program that does the heavy lifting).
|
||||
# The backend is also a QObject, which can be used from qml.
|
||||
# \returns Backend \type{Backend}
|
||||
@pyqtSlot(result = "QObject*")
|
||||
def getBackend(self):
|
||||
return self._backend
|
||||
qmlRegisterType(QUrl.fromLocalFile(path), "Cura", 1, 0, type_name)
|
||||
|
||||
def onSelectionChanged(self):
|
||||
if Selection.hasSelection():
|
||||
|
@ -725,7 +735,9 @@ class CuraApplication(QtApplication):
|
|||
else:
|
||||
# Default
|
||||
self.getController().setActiveTool("TranslateTool")
|
||||
if Preferences.getInstance().getValue("view/center_on_select"):
|
||||
|
||||
# Hack: QVector bindings are broken on PyQt 5.7.1 on Windows. This disables it being called at all.
|
||||
if Preferences.getInstance().getValue("view/center_on_select") and not Platform.isWindows():
|
||||
self._center_after_select = True
|
||||
else:
|
||||
if self.getController().getActiveTool():
|
||||
|
@ -801,6 +813,7 @@ class CuraApplication(QtApplication):
|
|||
|
||||
# Remove all selected objects from the scene.
|
||||
@pyqtSlot()
|
||||
@deprecated("Moved to CuraActions", "2.6")
|
||||
def deleteSelection(self):
|
||||
if not self.getController().getToolsEnabled():
|
||||
return
|
||||
|
@ -821,6 +834,7 @@ class CuraApplication(QtApplication):
|
|||
## Remove an object from the scene.
|
||||
# Note that this only removes an object if it is selected.
|
||||
@pyqtSlot("quint64")
|
||||
@deprecated("Use deleteSelection instead", "2.6")
|
||||
def deleteObject(self, object_id):
|
||||
if not self.getController().getToolsEnabled():
|
||||
return
|
||||
|
@ -844,27 +858,26 @@ class CuraApplication(QtApplication):
|
|||
op.push()
|
||||
|
||||
## Create a number of copies of existing object.
|
||||
# \param object_id
|
||||
# \param count number of copies
|
||||
# \param min_offset minimum offset to other objects.
|
||||
@pyqtSlot("quint64", int)
|
||||
def multiplyObject(self, object_id, count):
|
||||
@deprecated("Use CuraActions::multiplySelection", "2.6")
|
||||
def multiplyObject(self, object_id, count, min_offset = 8):
|
||||
node = self.getController().getScene().findObject(object_id)
|
||||
|
||||
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
|
||||
if not node:
|
||||
node = Selection.getSelectedObject(0)
|
||||
|
||||
if node:
|
||||
current_node = node
|
||||
# Find the topmost group
|
||||
while current_node.getParent() and current_node.getParent().callDecoration("isGroup"):
|
||||
current_node = current_node.getParent()
|
||||
while node.getParent() and node.getParent().callDecoration("isGroup"):
|
||||
node = node.getParent()
|
||||
|
||||
op = GroupedOperation()
|
||||
for _ in range(count):
|
||||
new_node = copy.deepcopy(current_node)
|
||||
op.addOperation(AddSceneNodeOperation(new_node, current_node.getParent()))
|
||||
op.push()
|
||||
job = MultiplyObjectsJob([node], count, min_offset)
|
||||
job.start()
|
||||
return
|
||||
|
||||
## Center object on platform.
|
||||
@pyqtSlot("quint64")
|
||||
@deprecated("Use CuraActions::centerSelection", "2.6")
|
||||
def centerObject(self, object_id):
|
||||
node = self.getController().getScene().findObject(object_id)
|
||||
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
|
||||
|
@ -979,6 +992,52 @@ class CuraApplication(QtApplication):
|
|||
op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0), Quaternion(), Vector(1, 1, 1)))
|
||||
op.push()
|
||||
|
||||
## Arrange all objects.
|
||||
@pyqtSlot()
|
||||
def arrangeAll(self):
|
||||
nodes = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if type(node) is not SceneNode:
|
||||
continue
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||
if not node.isSelectable():
|
||||
continue # i.e. node with layer data
|
||||
# Skip nodes that are too big
|
||||
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
|
||||
nodes.append(node)
|
||||
self.arrange(nodes, fixed_nodes = [])
|
||||
|
||||
## Arrange Selection
|
||||
@pyqtSlot()
|
||||
def arrangeSelection(self):
|
||||
nodes = Selection.getAllSelectedObjects()
|
||||
|
||||
# What nodes are on the build plate and are not being moved
|
||||
fixed_nodes = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if type(node) is not SceneNode:
|
||||
continue
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||
if not node.isSelectable():
|
||||
continue # i.e. node with layer data
|
||||
if node in nodes: # exclude selected node from fixed_nodes
|
||||
continue
|
||||
fixed_nodes.append(node)
|
||||
self.arrange(nodes, fixed_nodes)
|
||||
|
||||
## Arrange a set of nodes given a set of fixed nodes
|
||||
# \param nodes nodes that we have to place
|
||||
# \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes
|
||||
def arrange(self, nodes, fixed_nodes):
|
||||
job = ArrangeObjectsJob(nodes, fixed_nodes)
|
||||
job.start()
|
||||
|
||||
## Reload all mesh data on the screen from file.
|
||||
@pyqtSlot()
|
||||
def reloadAll(self):
|
||||
|
@ -1014,12 +1073,6 @@ class CuraApplication(QtApplication):
|
|||
|
||||
return log
|
||||
|
||||
recentFilesChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty("QVariantList", notify = recentFilesChanged)
|
||||
def recentFiles(self):
|
||||
return self._recent_files
|
||||
|
||||
@pyqtSlot("QStringList")
|
||||
def setExpandedCategories(self, categories):
|
||||
categories = list(set(categories))
|
||||
|
@ -1055,7 +1108,9 @@ class CuraApplication(QtApplication):
|
|||
transformation.setTranslation(zero_translation)
|
||||
transformed_mesh = mesh.getTransformed(transformation)
|
||||
center = transformed_mesh.getCenterPosition()
|
||||
object_centers.append(center)
|
||||
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)
|
||||
|
@ -1125,25 +1180,6 @@ class CuraApplication(QtApplication):
|
|||
|
||||
fileLoaded = pyqtSignal(str)
|
||||
|
||||
def _onJobFinished(self, job):
|
||||
if type(job) is not ReadMeshJob or not job.getResult():
|
||||
return
|
||||
|
||||
f = QUrl.fromLocalFile(job.getFileName())
|
||||
if f in self._recent_files:
|
||||
self._recent_files.remove(f)
|
||||
|
||||
self._recent_files.insert(0, f)
|
||||
if len(self._recent_files) > 10:
|
||||
del self._recent_files[10]
|
||||
|
||||
pref = ""
|
||||
for path in self._recent_files:
|
||||
pref += path.toLocalFile() + ";"
|
||||
|
||||
Preferences.getInstance().setValue("cura/recent_files", pref)
|
||||
self.recentFilesChanged.emit()
|
||||
|
||||
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()[0].getMeshData()
|
||||
|
@ -1238,6 +1274,10 @@ class CuraApplication(QtApplication):
|
|||
filename = job.getFileName()
|
||||
self._currently_loading_files.remove(filename)
|
||||
|
||||
root = self.getController().getScene().getRoot()
|
||||
arranger = Arrange.create(scene_root = root)
|
||||
min_offset = 8
|
||||
|
||||
for node in nodes:
|
||||
node.setSelectable(True)
|
||||
node.setName(os.path.basename(filename))
|
||||
|
@ -1258,9 +1298,24 @@ class CuraApplication(QtApplication):
|
|||
|
||||
scene = self.getController().getScene()
|
||||
|
||||
# If there is no convex hull for the node, start calculating it and continue.
|
||||
if not node.getDecorator(ConvexHullDecorator):
|
||||
node.addDecorator(ConvexHullDecorator())
|
||||
for child in node.getAllChildren():
|
||||
if not child.getDecorator(ConvexHullDecorator):
|
||||
child.addDecorator(ConvexHullDecorator())
|
||||
|
||||
if node.callDecoration("isSliceable"):
|
||||
# Only check position if it's not already blatantly obvious that it won't fit.
|
||||
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
|
||||
# Find node location
|
||||
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset)
|
||||
|
||||
# Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher
|
||||
node, _ = arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10)
|
||||
|
||||
op = AddSceneNodeOperation(node, scene.getRoot())
|
||||
op.push()
|
||||
|
||||
scene.sceneChanged.emit(node)
|
||||
|
||||
def addNonSliceableExtension(self, extension):
|
||||
|
@ -1271,15 +1326,24 @@ class CuraApplication(QtApplication):
|
|||
"""
|
||||
Checks if the given file URL is a valid project file.
|
||||
"""
|
||||
file_url_prefix = 'file:///'
|
||||
try:
|
||||
file_path = QUrl(file_url).toLocalFile()
|
||||
workspace_reader = self.getWorkspaceFileHandler().getReaderForFile(file_path)
|
||||
if workspace_reader is None:
|
||||
return False # non-project files won't get a reader
|
||||
|
||||
file_name = file_url
|
||||
if file_name.startswith(file_url_prefix):
|
||||
file_name = file_name[len(file_url_prefix):]
|
||||
result = workspace_reader.preRead(file_path, show_dialog=False)
|
||||
return result == WorkspaceReader.PreReadResult.accepted
|
||||
except Exception as e:
|
||||
Logger.log("e", "Could not check file %s: %s", file_url, e)
|
||||
return False
|
||||
|
||||
workspace_reader = self.getWorkspaceFileHandler().getReaderForFile(file_name)
|
||||
if workspace_reader is None:
|
||||
return False # non-project files won't get a reader
|
||||
def _onContextMenuRequested(self, x: float, y: float) -> None:
|
||||
# Ensure we select the object if we request a context menu over an object without having a selection.
|
||||
if not Selection.hasSelection():
|
||||
node = self.getController().getScene().findObject(self.getRenderer().getRenderPass("selection").getIdAtPosition(x, y))
|
||||
if node:
|
||||
while(node.getParent() and node.getParent().callDecoration("isGroup")):
|
||||
node = node.getParent()
|
||||
|
||||
result = workspace_reader.preRead(file_name, show_dialog=False)
|
||||
return result == WorkspaceReader.PreReadResult.accepted
|
||||
Selection.add(node)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue