Merge pull request #7971 from Ultimaker/CURA-7106-Speedup-multiple-objects-on-build-plate

CURA-7106 Speedup multiple objects on build plate
This commit is contained in:
Konstantinos Karmas 2020-06-29 12:36:02 +02:00 committed by GitHub
commit c6fd25e7e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 229 additions and 158 deletions

View File

@ -1,5 +1,6 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2019 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 QCoreApplication
from UM.Application import Application from UM.Application import Application
from UM.Job import Job from UM.Job import Job
@ -94,6 +95,7 @@ class ArrangeObjectsJob(Job):
status_message.setProgress((idx + 1) / len(nodes_arr) * 100) status_message.setProgress((idx + 1) / len(nodes_arr) * 100)
Job.yieldThread() Job.yieldThread()
QCoreApplication.processEvents()
grouped_operation.push() grouped_operation.push()

View File

@ -92,6 +92,8 @@ class BuildVolume(SceneNode):
self._adhesion_type = None # type: Any self._adhesion_type = None # type: Any
self._platform = Platform(self) self._platform = Platform(self)
self._edge_disallowed_size = None
self._build_volume_message = Message(catalog.i18nc("@info:status", self._build_volume_message = Message(catalog.i18nc("@info:status",
"The build volume height has been reduced due to the value of the" "The build volume height has been reduced due to the value of the"
" \"Print Sequence\" setting to prevent the gantry from colliding" " \"Print Sequence\" setting to prevent the gantry from colliding"
@ -106,8 +108,6 @@ class BuildVolume(SceneNode):
self._application.globalContainerStackChanged.connect(self._onStackChanged) self._application.globalContainerStackChanged.connect(self._onStackChanged)
self._onStackChanged()
self._engine_ready = False self._engine_ready = False
self._application.engineCreatedSignal.connect(self._onEngineCreated) self._application.engineCreatedSignal.connect(self._onEngineCreated)
@ -118,7 +118,7 @@ class BuildVolume(SceneNode):
self._scene_objects = set() # type: Set[SceneNode] self._scene_objects = set() # type: Set[SceneNode]
self._scene_change_timer = QTimer() self._scene_change_timer = QTimer()
self._scene_change_timer.setInterval(100) self._scene_change_timer.setInterval(200)
self._scene_change_timer.setSingleShot(True) self._scene_change_timer.setSingleShot(True)
self._scene_change_timer.timeout.connect(self._onSceneChangeTimerFinished) self._scene_change_timer.timeout.connect(self._onSceneChangeTimerFinished)
@ -747,6 +747,7 @@ class BuildVolume(SceneNode):
self._error_areas = [] self._error_areas = []
used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks() used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks()
self._edge_disallowed_size = None # Force a recalculation
disallowed_border_size = self.getEdgeDisallowedSize() disallowed_border_size = self.getEdgeDisallowedSize()
result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) # Normal machine disallowed areas can always be added. result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) # Normal machine disallowed areas can always be added.
@ -1124,6 +1125,9 @@ class BuildVolume(SceneNode):
if not self._global_container_stack or not self._global_container_stack.extruderList: if not self._global_container_stack or not self._global_container_stack.extruderList:
return 0 return 0
if self._edge_disallowed_size is not None:
return self._edge_disallowed_size
container_stack = self._global_container_stack container_stack = self._global_container_stack
used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks() used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks()
@ -1139,8 +1143,8 @@ class BuildVolume(SceneNode):
# Now combine our different pieces of data to get the final border size. # 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 the bed adhesion, since the bed adhesion goes around support.
# Support expansion is added to farthest shield distance, since the shields go 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) self._edge_disallowed_size = max(move_from_wall_radius, support_expansion + farthest_shield_distance, support_expansion + bed_adhesion_size)
return border_size return self._edge_disallowed_size
def _clamp(self, value, min_value, max_value): def _clamp(self, value, min_value, max_value):
return max(min(value, max_value), min_value) return max(min(value, max_value), min_value)

View File

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2020 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 time import time
@ -14,6 +14,7 @@ from UM.Settings.Validator import ValidatorState
import cura.CuraApplication import cura.CuraApplication
class MachineErrorChecker(QObject): class MachineErrorChecker(QObject):
"""This class performs setting error checks for the currently active machine. """This class performs setting error checks for the currently active machine.
@ -50,6 +51,8 @@ class MachineErrorChecker(QObject):
self._error_check_timer.setInterval(100) self._error_check_timer.setInterval(100)
self._error_check_timer.setSingleShot(True) self._error_check_timer.setSingleShot(True)
self._keys_to_check = set() # type: Set[str]
def initialize(self) -> None: def initialize(self) -> None:
self._error_check_timer.timeout.connect(self._rescheduleCheck) self._error_check_timer.timeout.connect(self._rescheduleCheck)
@ -103,6 +106,7 @@ class MachineErrorChecker(QObject):
if property_name != "value": if property_name != "value":
return return
self._keys_to_check.add(key)
self.startErrorCheck() self.startErrorCheck()
def startErrorCheck(self, *args: Any) -> None: def startErrorCheck(self, *args: Any) -> None:
@ -140,7 +144,10 @@ class MachineErrorChecker(QObject):
# Populate the (stack, key) tuples to check # Populate the (stack, key) tuples to check
self._stacks_and_keys_to_check = deque() self._stacks_and_keys_to_check = deque()
for stack in global_stack.extruderList: for stack in global_stack.extruderList:
for key in stack.getAllKeys(): if not self._keys_to_check:
self._keys_to_check = stack.getAllKeys()
for key in self._keys_to_check:
self._stacks_and_keys_to_check.append((stack, key)) self._stacks_and_keys_to_check.append((stack, key))
self._application.callLater(self._checkStack) self._application.callLater(self._checkStack)
@ -181,18 +188,25 @@ class MachineErrorChecker(QObject):
validator = validator_type(key) validator = validator_type(key)
validation_state = validator(stack) validation_state = validator(stack)
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError, ValidatorState.Invalid): if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError, ValidatorState.Invalid):
# Finish # Since we don't know if any of the settings we didn't check is has an error value, store the list for the
self._setResult(True) # next check.
keys_to_recheck = {setting_key for stack, setting_key in self._stacks_and_keys_to_check}
keys_to_recheck.add(key)
self._setResult(True, keys_to_recheck = keys_to_recheck)
return return
# Schedule the check for the next key # Schedule the check for the next key
self._application.callLater(self._checkStack) self._application.callLater(self._checkStack)
def _setResult(self, result: bool) -> None: def _setResult(self, result: bool, keys_to_recheck = None) -> None:
if result != self._has_errors: if result != self._has_errors:
self._has_errors = result self._has_errors = result
self.hasErrorUpdated.emit() self.hasErrorUpdated.emit()
self._machine_manager.stacksValidationChanged.emit() self._machine_manager.stacksValidationChanged.emit()
if keys_to_recheck is None:
self._keys_to_check = set()
else:
self._keys_to_check = keys_to_recheck
self._need_to_check = False self._need_to_check = False
self._check_in_progress = False self._check_in_progress = False
self.needToWaitForResultChanged.emit() self.needToWaitForResultChanged.emit()

View File

@ -4,6 +4,8 @@
import copy import copy
from typing import List from typing import List
from PyQt5.QtCore import QCoreApplication
from UM.Job import Job from UM.Job import Job
from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.GroupedOperation import GroupedOperation
from UM.Message import Message from UM.Message import Message
@ -93,8 +95,9 @@ class MultiplyObjectsJob(Job):
nodes.append(new_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)
QCoreApplication.processEvents()
Job.yieldThread() Job.yieldThread()
QCoreApplication.processEvents()
Job.yieldThread() Job.yieldThread()
if nodes: if nodes:

View File

@ -54,7 +54,7 @@ class PickingPass(RenderPass):
# Fill up the batch with objects that can be sliced. ` # Fill up the batch with objects that can be sliced. `
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible(): if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
batch.addItem(node.getWorldTransformation(), node.getMeshData()) batch.addItem(node.getWorldTransformation(copy = False), node.getMeshData())
self.bind() self.bind()
batch.render(self._scene.getActiveCamera()) batch.render(self._scene.getActiveCamera())

View File

@ -114,12 +114,12 @@ class PreviewPass(RenderPass):
1.0] 1.0]
uniforms["diffuse_color"] = prettier_color(diffuse_color) uniforms["diffuse_color"] = prettier_color(diffuse_color)
uniforms["diffuse_color_2"] = diffuse_color2 uniforms["diffuse_color_2"] = diffuse_color2
batch_support_mesh.addItem(node.getWorldTransformation(), node.getMeshData(), uniforms = uniforms) batch_support_mesh.addItem(node.getWorldTransformation(copy = False), node.getMeshData(), uniforms = uniforms)
else: else:
# Normal scene node # Normal scene node
uniforms = {} uniforms = {}
uniforms["diffuse_color"] = prettier_color(cast(CuraSceneNode, node).getDiffuseColor()) uniforms["diffuse_color"] = prettier_color(cast(CuraSceneNode, node).getDiffuseColor())
batch.addItem(node.getWorldTransformation(), node.getMeshData(), uniforms = uniforms) batch.addItem(node.getWorldTransformation(copy = False), node.getMeshData(), uniforms = uniforms)
self.bind() self.bind()

View File

@ -53,6 +53,8 @@ class ConvexHullDecorator(SceneNodeDecorator):
CuraApplication.getInstance().getController().toolOperationStarted.connect(self._onChanged) CuraApplication.getInstance().getController().toolOperationStarted.connect(self._onChanged)
CuraApplication.getInstance().getController().toolOperationStopped.connect(self._onChanged) CuraApplication.getInstance().getController().toolOperationStopped.connect(self._onChanged)
self._root = Application.getInstance().getController().getScene().getRoot()
self._onGlobalStackChanged() self._onGlobalStackChanged()
def createRecomputeConvexHullTimer(self) -> None: def createRecomputeConvexHullTimer(self) -> None:
@ -198,23 +200,16 @@ class ConvexHullDecorator(SceneNodeDecorator):
CuraApplication.getInstance().callLater(self.recomputeConvexHullDelayed) CuraApplication.getInstance().callLater(self.recomputeConvexHullDelayed)
def recomputeConvexHull(self) -> None: def recomputeConvexHull(self) -> None:
controller = Application.getInstance().getController() if self._node is None or not self.__isDescendant(self._root, self._node):
root = controller.getScene().getRoot()
if self._node is None or controller.isToolOperationActive() or not self.__isDescendant(root, self._node):
# If the tool operation is still active, we need to compute the convex hull later after the controller is
# no longer active.
if controller.isToolOperationActive():
self.recomputeConvexHullDelayed()
return
if self._convex_hull_node: if self._convex_hull_node:
# Convex hull node still exists, but the node is removed or no longer in the scene.
self._convex_hull_node.setParent(None) self._convex_hull_node.setParent(None)
self._convex_hull_node = None self._convex_hull_node = None
return return
if self._convex_hull_node: if self._convex_hull_node:
self._convex_hull_node.setParent(None) self._convex_hull_node.setParent(None)
hull_node = ConvexHullNode.ConvexHullNode(self._node, self.getPrintingArea(), self._raft_thickness, root) hull_node = ConvexHullNode.ConvexHullNode(self._node, self.getPrintingArea(), self._raft_thickness, self._root)
self._convex_hull_node = hull_node self._convex_hull_node = hull_node
def _onSettingValueChanged(self, key: str, property_name: str) -> None: def _onSettingValueChanged(self, key: str, property_name: str) -> None:
@ -273,7 +268,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
if mesh is None: if mesh is None:
return Polygon([]) # Node has no mesh data, so just return an empty Polygon. return Polygon([]) # Node has no mesh data, so just return an empty Polygon.
world_transform = self._node.getWorldTransformation() world_transform = self._node.getWorldTransformation(copy= False)
# Check the cache # Check the cache
if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform: if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform:

View File

@ -57,7 +57,14 @@ class ConvexHullNode(SceneNode):
self._hull = hull self._hull = hull
if self._hull: if self._hull:
hull_mesh_builder = MeshBuilder() hull_mesh_builder = MeshBuilder()
if self._thickness == 0:
if hull_mesh_builder.addConvexPolygon(
self._hull.getPoints()[::], # bottom layer is reversed
self._mesh_height, color = self._color):
hull_mesh = hull_mesh_builder.build()
self.setMeshData(hull_mesh)
else:
if hull_mesh_builder.addConvexPolygonExtrusion( if hull_mesh_builder.addConvexPolygonExtrusion(
self._hull.getPoints()[::-1], # bottom layer is reversed self._hull.getPoints()[::-1], # bottom layer is reversed
self._mesh_height - thickness, self._mesh_height, color = self._color): self._mesh_height - thickness, self._mesh_height, color = self._color):
@ -79,11 +86,11 @@ class ConvexHullNode(SceneNode):
ConvexHullNode.shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "transparent_object.shader")) ConvexHullNode.shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "transparent_object.shader"))
ConvexHullNode.shader.setUniformValue("u_diffuseColor", self._color) ConvexHullNode.shader.setUniformValue("u_diffuseColor", self._color)
ConvexHullNode.shader.setUniformValue("u_opacity", 0.6) ConvexHullNode.shader.setUniformValue("u_opacity", 0.6)
batch = renderer.getNamedBatch("convex_hull_node")
if self.getParent(): if not batch:
if self.getMeshData() and isinstance(self._node, SceneNode) and self._node.callDecoration("getBuildPlateNumber") == Application.getInstance().getMultiBuildPlateModel().activeBuildPlate: batch = renderer.createRenderBatch(transparent = True, shader = ConvexHullNode.shader, backface_cull = True, sort = -8)
# The object itself (+ adhesion in one-at-a-time mode) renderer.addRenderBatch(batch, name = "convex_hull_node")
renderer.queueNode(self, transparent = True, shader = ConvexHullNode.shader, backface_cull = True, sort = -8) batch.addItem(self.getWorldTransformation(copy = False), self.getMeshData())
if self._convex_hull_head_mesh: if self._convex_hull_head_mesh:
# The full head. Rendered as a hint to the user: If this area overlaps another object A; this object # The full head. Rendered as a hint to the user: If this area overlaps another object A; this object
# cannot be printed after A, because the head would hit A while printing the current object # cannot be printed after A, because the head would hit A while printing the current object
@ -97,7 +104,3 @@ class ConvexHullNode(SceneNode):
convex_hull_head_builder = MeshBuilder() convex_hull_head_builder = MeshBuilder()
convex_hull_head_builder.addConvexPolygon(convex_hull_head.getPoints(), self._mesh_height - self._thickness) convex_hull_head_builder.addConvexPolygon(convex_hull_head.getPoints(), self._mesh_height - self._thickness)
self._convex_hull_head_mesh = convex_hull_head_builder.build() self._convex_hull_head_mesh = convex_hull_head_builder.build()
if not node:
return

View File

@ -118,7 +118,7 @@ class CuraSceneNode(SceneNode):
self._aabb = None self._aabb = None
if self._mesh_data: if self._mesh_data:
self._aabb = self._mesh_data.getExtents(self.getWorldTransformation()) self._aabb = self._mesh_data.getExtents(self.getWorldTransformation(copy = False))
else: # If there is no mesh_data, use a bounding box that encompasses the local (0,0,0) else: # If there is no mesh_data, use a bounding box that encompasses the local (0,0,0)
position = self.getWorldPosition() position = self.getWorldPosition()
self._aabb = AxisAlignedBox(minimum = position, maximum = position) self._aabb = AxisAlignedBox(minimum = position, maximum = position)
@ -139,7 +139,7 @@ class CuraSceneNode(SceneNode):
"""Taken from SceneNode, but replaced SceneNode with CuraSceneNode""" """Taken from SceneNode, but replaced SceneNode with CuraSceneNode"""
copy = CuraSceneNode(no_setting_override = True) # Setting override will be added later copy = CuraSceneNode(no_setting_override = True) # Setting override will be added later
copy.setTransformation(self.getLocalTransformation()) copy.setTransformation(self.getLocalTransformation(copy= False))
copy.setMeshData(self._mesh_data) copy.setMeshData(self._mesh_data)
copy.setVisible(cast(bool, deepcopy(self._visible, memo))) copy.setVisible(cast(bool, deepcopy(self._visible, memo)))
copy._selectable = cast(bool, deepcopy(self._selectable, memo)) copy._selectable = cast(bool, deepcopy(self._selectable, memo))

View File

@ -204,33 +204,34 @@ class ExtruderManager(QObject):
# If no extruders are registered in the extruder manager yet, return an empty array # If no extruders are registered in the extruder manager yet, return an empty array
if len(self.extruderIds) == 0: if len(self.extruderIds) == 0:
return [] return []
number_active_extruders = len([extruder for extruder in self.getActiveExtruderStacks() if extruder.isEnabled])
# Get the extruders of all printable meshes in the scene # Get the extruders of all printable meshes in the scene
meshes = [node for node in DepthFirstIterator(scene_root) if isinstance(node, SceneNode) and node.isSelectable()] #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. nodes = [node for node in DepthFirstIterator(scene_root) if node.isSelectable() and not node.callDecoration("isAntiOverhangMesh") and not node.callDecoration("isSupportMesh")] #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
# Exclude anti-overhang meshes for node in nodes:
mesh_list = [] extruder_stack_id = node.callDecoration("getActiveExtruder")
for mesh in meshes:
stack = mesh.callDecoration("getStack")
if stack is not None and (stack.getProperty("anti_overhang_mesh", "value") or stack.getProperty("support_mesh", "value")):
continue
mesh_list.append(mesh)
for mesh in mesh_list:
extruder_stack_id = mesh.callDecoration("getActiveExtruder")
if not extruder_stack_id: if not extruder_stack_id:
# No per-object settings for this node # No per-object settings for this node
extruder_stack_id = self.extruderIds["0"] extruder_stack_id = self.extruderIds["0"]
used_extruder_stack_ids.add(extruder_stack_id) used_extruder_stack_ids.add(extruder_stack_id)
if len(used_extruder_stack_ids) == number_active_extruders:
# We're already done. Stop looking.
# Especially with a lot of models on the buildplate, this will speed up things rather dramatically.
break
# Get whether any of them use support. # Get whether any of them use support.
stack_to_use = mesh.callDecoration("getStack") # if there is a per-mesh stack, we use it stack_to_use = node.callDecoration("getStack") # if there is a per-mesh stack, we use it
if not stack_to_use: if not stack_to_use:
# if there is no per-mesh stack, we use the build extruder for this mesh # if there is no per-mesh stack, we use the build extruder for this mesh
stack_to_use = container_registry.findContainerStacks(id = extruder_stack_id)[0] stack_to_use = container_registry.findContainerStacks(id = extruder_stack_id)[0]
if not support_enabled:
support_enabled |= stack_to_use.getProperty("support_enable", "value") support_enabled |= stack_to_use.getProperty("support_enable", "value")
if not support_bottom_enabled:
support_bottom_enabled |= stack_to_use.getProperty("support_bottom_enable", "value") support_bottom_enabled |= stack_to_use.getProperty("support_bottom_enable", "value")
if not support_roof_enabled:
support_roof_enabled |= stack_to_use.getProperty("support_roof_enable", "value") support_roof_enabled |= stack_to_use.getProperty("support_roof_enable", "value")
# Check limit to extruders # Check limit to extruders

View File

@ -35,7 +35,7 @@ class SettingOverrideDecorator(SceneNodeDecorator):
""" """
_non_thumbnail_visible_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh", "support_mesh"} _non_thumbnail_visible_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh", "support_mesh"}
def __init__(self): def __init__(self, *, force_update = True):
super().__init__() super().__init__()
self._stack = PerObjectContainerStack(container_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.
@ -46,6 +46,10 @@ class SettingOverrideDecorator(SceneNodeDecorator):
self._is_non_printing_mesh = False self._is_non_printing_mesh = False
self._is_non_thumbnail_visible_mesh = False self._is_non_thumbnail_visible_mesh = False
self._is_support_mesh = False
self._is_cutting_mesh = False
self._is_infill_mesh = False
self._is_anti_overhang_mesh = False
self._stack.propertyChanged.connect(self._onSettingChanged) self._stack.propertyChanged.connect(self._onSettingChanged)
@ -53,13 +57,14 @@ class SettingOverrideDecorator(SceneNodeDecorator):
Application.getInstance().globalContainerStackChanged.connect(self._updateNextStack) Application.getInstance().globalContainerStackChanged.connect(self._updateNextStack)
self.activeExtruderChanged.connect(self._updateNextStack) self.activeExtruderChanged.connect(self._updateNextStack)
if force_update:
self._updateNextStack() self._updateNextStack()
def _generateUniqueName(self): def _generateUniqueName(self):
return "SettingOverrideInstanceContainer-%s" % uuid.uuid1() return "SettingOverrideInstanceContainer-%s" % uuid.uuid1()
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
deep_copy = SettingOverrideDecorator() deep_copy = SettingOverrideDecorator(force_update = False)
"""Create a fresh decorator object""" """Create a fresh decorator object"""
instance_container = copy.deepcopy(self._stack.getContainer(0), memo) instance_container = copy.deepcopy(self._stack.getContainer(0), memo)
@ -74,11 +79,6 @@ class SettingOverrideDecorator(SceneNodeDecorator):
# Properly set the right extruder on the copy # Properly set the right extruder on the copy
deep_copy.setActiveExtruder(self._extruder_stack) deep_copy.setActiveExtruder(self._extruder_stack)
# use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh"
# has not been updated yet.
deep_copy._is_non_printing_mesh = self._evaluateIsNonPrintingMesh()
deep_copy._is_non_thumbnail_visible_mesh = self._evaluateIsNonThumbnailVisibleMesh()
return deep_copy return deep_copy
def getActiveExtruder(self): def getActiveExtruder(self):
@ -104,7 +104,7 @@ class SettingOverrideDecorator(SceneNodeDecorator):
""" """
# for support_meshes, always use the support_extruder # for support_meshes, always use the support_extruder
if self.getStack().getProperty("support_mesh", "value"): if self._is_support_mesh:
global_container_stack = Application.getInstance().getGlobalContainerStack() global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack: if global_container_stack:
return str(global_container_stack.getProperty("support_extruder_nr", "value")) return str(global_container_stack.getProperty("support_extruder_nr", "value"))
@ -114,6 +114,30 @@ class SettingOverrideDecorator(SceneNodeDecorator):
container_stack = containers[0] container_stack = containers[0]
return container_stack.getMetaDataEntry("position", default=None) return container_stack.getMetaDataEntry("position", default=None)
def isCuttingMesh(self):
return self._is_cutting_mesh
def isSupportMesh(self):
return self._is_support_mesh
def isInfillMesh(self):
return self._is_infill_mesh
def isAntiOverhangMesh(self):
return self._is_anti_overhang_mesh
def _evaluateAntiOverhangMesh(self):
return bool(self._stack.userChanges.getProperty("anti_overhang_mesh", "value"))
def _evaluateIsCuttingMesh(self):
return bool(self._stack.userChanges.getProperty("cutting_mesh", "value"))
def _evaluateIsSupportMesh(self):
return bool(self._stack.userChanges.getProperty("support_mesh", "value"))
def _evaluateInfillMesh(self):
return bool(self._stack.userChanges.getProperty("infill_mesh", "value"))
def isNonPrintingMesh(self): def isNonPrintingMesh(self):
return self._is_non_printing_mesh return self._is_non_printing_mesh
@ -132,6 +156,16 @@ class SettingOverrideDecorator(SceneNodeDecorator):
# Trigger slice/need slicing if the value has changed. # Trigger slice/need slicing if the value has changed.
self._is_non_printing_mesh = self._evaluateIsNonPrintingMesh() self._is_non_printing_mesh = self._evaluateIsNonPrintingMesh()
self._is_non_thumbnail_visible_mesh = self._evaluateIsNonThumbnailVisibleMesh() self._is_non_thumbnail_visible_mesh = self._evaluateIsNonThumbnailVisibleMesh()
if setting_key == "anti_overhang_mesh":
self._is_anti_overhang_mesh = self._evaluateAntiOverhangMesh()
elif setting_key == "support_mesh":
self._is_support_mesh = self._evaluateIsSupportMesh()
elif setting_key == "cutting_mesh":
self._is_cutting_mesh = self._evaluateIsCuttingMesh()
elif setting_key == "infill_mesh":
self._is_infill_mesh = self._evaluateInfillMesh()
Application.getInstance().getBackend().needsSlicing() Application.getInstance().getBackend().needsSlicing()
Application.getInstance().getBackend().tickle() Application.getInstance().getBackend().tickle()

View File

@ -98,7 +98,8 @@ class ObjectsModel(ListModel):
return True return True
def _renameNodes(self, node_info_dict: Dict[str, _NodeInfo]) -> List[SceneNode]: @staticmethod
def _renameNodes(node_info_dict: Dict[str, _NodeInfo]) -> List[SceneNode]:
# Go through all names and find out the names for all nodes that need to be renamed. # Go through all names and find out the names for all nodes that need to be renamed.
all_nodes = [] # type: List[SceneNode] all_nodes = [] # type: List[SceneNode]
for name, node_info in node_info_dict.items(): for name, node_info in node_info_dict.items():
@ -118,9 +119,7 @@ class ObjectsModel(ListModel):
else: else:
new_group_name = "{0}#{1}".format(name, current_index) new_group_name = "{0}#{1}".format(name, current_index)
old_name = node.getName()
node.setName(new_group_name) node.setName(new_group_name)
Logger.log("d", "Node [%s] renamed to [%s]", old_name, new_group_name)
all_nodes.append(node) all_nodes.append(node)
return all_nodes return all_nodes
@ -186,11 +185,18 @@ class ObjectsModel(ListModel):
if per_object_stack: if per_object_stack:
per_object_settings_count = per_object_stack.getTop().getNumInstances() per_object_settings_count = per_object_stack.getTop().getNumInstances()
for mesh_type in ["anti_overhang_mesh", "infill_mesh", "cutting_mesh", "support_mesh"]: if node.callDecoration("isAntiOverhangMesh"):
if per_object_stack.getProperty(mesh_type, "value"): node_mesh_type = "anti_overhang_mesh"
node_mesh_type = mesh_type per_object_settings_count -= 1 # do not count this mesh type setting
elif node.callDecoration("isSupportMesh"):
node_mesh_type = "support_mesh"
per_object_settings_count -= 1 # do not count this mesh type setting
elif node.callDecoration("isCuttingMesh"):
node_mesh_type = "cutting_mesh"
per_object_settings_count -= 1 # do not count this mesh type setting
elif node.callDecoration("isInfillMesh"):
node_mesh_type = "infill_mesh"
per_object_settings_count -= 1 # do not count this mesh type setting per_object_settings_count -= 1 # do not count this mesh type setting
break
if per_object_settings_count > 0: if per_object_settings_count > 0:
if node_mesh_type == "support_mesh": if node_mesh_type == "support_mesh":

View File

@ -29,7 +29,7 @@ class XRayPass(RenderPass):
batch = RenderBatch(self._shader, type = RenderBatch.RenderType.NoType, backface_cull = False, blend_mode = RenderBatch.BlendMode.Additive) batch = RenderBatch(self._shader, type = RenderBatch.RenderType.NoType, backface_cull = False, blend_mode = RenderBatch.BlendMode.Additive)
for node in DepthFirstIterator(self._scene.getRoot()): for node in DepthFirstIterator(self._scene.getRoot()):
if isinstance(node, CuraSceneNode) and node.getMeshData() and node.isVisible(): if isinstance(node, CuraSceneNode) and node.getMeshData() and node.isVisible():
batch.addItem(node.getWorldTransformation(), node.getMeshData()) batch.addItem(node.getWorldTransformation(copy = False), node.getMeshData())
self.bind() self.bind()

View File

@ -8,12 +8,14 @@ import time
from typing import Any, cast, Dict, List, Optional, Set from typing import Any, cast, Dict, List, Optional, Set
import re import re
import Arcus #For typing. import Arcus #For typing.
from PyQt5.QtCore import QCoreApplication
from UM.Job import Job from UM.Job import Job
from UM.Logger import Logger from UM.Logger import Logger
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Settings.ContainerStack import ContainerStack #For typing. from UM.Settings.ContainerStack import ContainerStack #For typing.
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.Interfaces import ContainerInterface
from UM.Settings.SettingDefinition import SettingDefinition from UM.Settings.SettingDefinition import SettingDefinition
from UM.Settings.SettingRelation import SettingRelation #For typing. from UM.Settings.SettingRelation import SettingRelation #For typing.
@ -352,8 +354,7 @@ class StartSliceJob(Job):
result = {} result = {}
for key in stack.getAllKeys(): for key in stack.getAllKeys():
value = stack.getProperty(key, "value") result[key] = stack.getProperty(key, "value")
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.
@ -373,9 +374,11 @@ class StartSliceJob(Job):
self._all_extruders_settings = { self._all_extruders_settings = {
"-1": self._buildReplacementTokens(global_stack) "-1": self._buildReplacementTokens(global_stack)
} }
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
for extruder_stack in ExtruderManager.getInstance().getActiveExtruderStacks(): for extruder_stack in ExtruderManager.getInstance().getActiveExtruderStacks():
extruder_nr = extruder_stack.getProperty("extruder_nr", "value") extruder_nr = extruder_stack.getProperty("extruder_nr", "value")
self._all_extruders_settings[str(extruder_nr)] = self._buildReplacementTokens(extruder_stack) self._all_extruders_settings[str(extruder_nr)] = self._buildReplacementTokens(extruder_stack)
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
def _expandGcodeTokens(self, value: str, default_extruder_nr: int = -1) -> str: def _expandGcodeTokens(self, value: str, default_extruder_nr: int = -1) -> str:
"""Replace setting tokens in a piece of g-code. """Replace setting tokens in a piece of g-code.
@ -420,9 +423,14 @@ class StartSliceJob(Job):
settings["machine_extruder_start_code"] = self._expandGcodeTokens(settings["machine_extruder_start_code"], extruder_nr) settings["machine_extruder_start_code"] = self._expandGcodeTokens(settings["machine_extruder_start_code"], extruder_nr)
settings["machine_extruder_end_code"] = self._expandGcodeTokens(settings["machine_extruder_end_code"], extruder_nr) settings["machine_extruder_end_code"] = self._expandGcodeTokens(settings["machine_extruder_end_code"], extruder_nr)
global_definition = cast(ContainerInterface, cast(ContainerStack, stack.getNextStack()).getBottom())
own_definition = cast(ContainerInterface, stack.getBottom())
for key, value in settings.items(): for key, value in settings.items():
# Do not send settings that are not settable_per_extruder. # Do not send settings that are not settable_per_extruder.
if not stack.getProperty(key, "settable_per_extruder"): # Since these can only be set in definition files, we only have to ask there.
if not global_definition.getProperty(key, "settable_per_extruder") and \
not own_definition.getProperty(key, "settable_per_extruder"):
continue continue
setting = message.getMessage("settings").addRepeatedMessage("settings") setting = message.getMessage("settings").addRepeatedMessage("settings")
setting.name = key setting.name = key
@ -454,11 +462,10 @@ class StartSliceJob(Job):
print_temperature_settings = ["material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"] print_temperature_settings = ["material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"]
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(print_temperature_settings) # match {setting} as well as {setting, extruder_nr} pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(print_temperature_settings) # match {setting} as well as {setting, extruder_nr}
settings["material_print_temp_prepend"] = re.search(pattern, start_gcode) == None settings["material_print_temp_prepend"] = re.search(pattern, start_gcode) == None
# Replace the setting tokens in start and end g-code. # Replace the setting tokens in start and end g-code.
# Use values from the first used extruder by default so we get the expected temperatures # Use values from the first used extruder by default so we get the expected temperatures
initial_extruder_stack = CuraApplication.getInstance().getExtruderManager().getUsedExtruderStacks()[0] initial_extruder_nr = CuraApplication.getInstance().getExtruderManager().getInitialExtruderNr()
initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value")
settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], initial_extruder_nr) settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], initial_extruder_nr)
settings["machine_end_gcode"] = self._expandGcodeTokens(settings["machine_end_gcode"], initial_extruder_nr) settings["machine_end_gcode"] = self._expandGcodeTokens(settings["machine_end_gcode"], initial_extruder_nr)

View File

@ -199,7 +199,7 @@ class SliceInfo(QObject, Extension):
"maximum": {"x": bounding_box.maximum.x, "maximum": {"x": bounding_box.maximum.x,
"y": bounding_box.maximum.y, "y": bounding_box.maximum.y,
"z": bounding_box.maximum.z}} "z": bounding_box.maximum.z}}
model["transformation"] = {"data": str(node.getWorldTransformation().getData()).replace("\n", "")} model["transformation"] = {"data": str(node.getWorldTransformation(copy = False).getData()).replace("\n", "")}
extruder_position = node.callDecoration("getActiveExtruderPosition") extruder_position = node.callDecoration("getActiveExtruderPosition")
model["extruder"] = 0 if extruder_position is None else int(extruder_position) model["extruder"] = 0 if extruder_position is None else int(extruder_position)

View File

@ -64,7 +64,7 @@ class SolidView(View):
self._old_layer_bindings = None self._old_layer_bindings = None
self._next_xray_checking_time = time.time() self._next_xray_checking_time = time.time()
self._xray_checking_update_time = 1.0 # seconds self._xray_checking_update_time = 30.0 # seconds
self._xray_warning_cooldown = 60 * 10 # reshow Model error message every 10 minutes self._xray_warning_cooldown = 60 * 10 # reshow Model error message every 10 minutes
self._xray_warning_message = Message( self._xray_warning_message = Message(
catalog.i18nc("@info:status", "Your model is not manifold. The highlighted areas indicate either missing or extraneous surfaces."), catalog.i18nc("@info:status", "Your model is not manifold. The highlighted areas indicate either missing or extraneous surfaces."),
@ -103,7 +103,9 @@ class SolidView(View):
except IndexError: except IndexError:
pass pass
else: else:
self._support_angle = support_angle_stack.getProperty("support_angle", "value") angle = support_angle_stack.getProperty("support_angle", "value")
if angle is not None:
self._support_angle = angle
def _checkSetup(self): def _checkSetup(self):
if not self._extruders_model: if not self._extruders_model:
@ -178,16 +180,21 @@ class SolidView(View):
if global_container_stack: if global_container_stack:
if Application.getInstance().getPreferences().getValue("view/show_overhang"): if Application.getInstance().getPreferences().getValue("view/show_overhang"):
# Make sure the overhang angle is valid before passing it to the shader # Make sure the overhang angle is valid before passing it to the shader
if self._support_angle is not None and self._support_angle >= 0 and self._support_angle <= 90: if self._support_angle >= 0 and self._support_angle <= 90:
self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(90 - self._support_angle))) self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(90 - self._support_angle)))
else: else:
self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0))) #Overhang angle of 0 causes no area at all to be marked as overhang. self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0))) #Overhang angle of 0 causes no area at all to be marked as overhang.
else: else:
self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0))) self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0)))
disabled_batch = renderer.createRenderBatch(shader = self._disabled_shader)
normal_object_batch = renderer.createRenderBatch(shader = self._enabled_shader)
renderer.addRenderBatch(disabled_batch)
renderer.addRenderBatch(normal_object_batch)
for node in DepthFirstIterator(scene.getRoot()): for node in DepthFirstIterator(scene.getRoot()):
if not node.render(renderer): if node.render(renderer):
if node.getMeshData() and node.isVisible() and not node.callDecoration("getLayerData"): continue
if node.getMeshData() and node.isVisible():
uniforms = {} uniforms = {}
shade_factor = 1.0 shade_factor = 1.0
@ -198,11 +205,6 @@ class SolidView(View):
extruder_index = "0" extruder_index = "0"
extruder_index = int(extruder_index) extruder_index = int(extruder_index)
# Use the support extruder instead of the active extruder if this is a support_mesh
if per_mesh_stack:
if per_mesh_stack.getProperty("support_mesh", "value"):
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"]
except KeyError: except KeyError:
@ -229,13 +231,13 @@ class SolidView(View):
pass pass
if node.callDecoration("isNonPrintingMesh"): if node.callDecoration("isNonPrintingMesh"):
if per_mesh_stack and (per_mesh_stack.getProperty("infill_mesh", "value") or per_mesh_stack.getProperty("cutting_mesh", "value")): if per_mesh_stack and (node.callDecoration("isInfillMesh") or node.callDecoration("isCuttingMesh")):
renderer.queueNode(node, shader = self._non_printing_shader, uniforms = uniforms, transparent = True) renderer.queueNode(node, shader = self._non_printing_shader, uniforms = uniforms, transparent = True)
else: else:
renderer.queueNode(node, shader = self._non_printing_shader, transparent = True) renderer.queueNode(node, shader = self._non_printing_shader, transparent = True)
elif getattr(node, "_outside_buildarea", False): elif getattr(node, "_outside_buildarea", False):
renderer.queueNode(node, shader = self._disabled_shader) disabled_batch.addItem(node.getWorldTransformation(copy = False), node.getMeshData())
elif per_mesh_stack and per_mesh_stack.getProperty("support_mesh", "value"): elif per_mesh_stack and node.callDecoration("isSupportMesh"):
# Render support meshes with a vertical stripe that is darker # Render support meshes with a vertical stripe that is darker
shade_factor = 0.6 shade_factor = 0.6
uniforms["diffuse_color_2"] = [ uniforms["diffuse_color_2"] = [
@ -246,7 +248,7 @@ class SolidView(View):
] ]
renderer.queueNode(node, shader = self._support_mesh_shader, uniforms = uniforms) renderer.queueNode(node, shader = self._support_mesh_shader, uniforms = uniforms)
else: else:
renderer.queueNode(node, shader = self._enabled_shader, uniforms = uniforms) normal_object_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData(), uniforms=uniforms)
if node.callDecoration("isGroup") and Selection.isSelected(node): if node.callDecoration("isGroup") and Selection.isSelected(node):
renderer.queueNode(scene.getRoot(), mesh = node.getBoundingBoxMesh(), mode = RenderBatch.RenderMode.LineLoop) renderer.queueNode(scene.getRoot(), mesh = node.getBoundingBoxMesh(), mode = RenderBatch.RenderMode.LineLoop)