Merge remote-tracking branch 'origin/main' into CURA-12580_paint-on-support

This commit is contained in:
Erwan MATHIEU 2025-12-19 14:29:47 +01:00
commit 1145adfc3a
116 changed files with 6100 additions and 1916 deletions

View file

@ -13,6 +13,14 @@ on:
default: true
required: false
type: boolean
cura_conan_version:
description: 'Cura Conan Version (optional, overrides discovered packages)'
default: ''
type: string
package_overrides:
description: 'List of specific packages to be used (space-separated, in addition to discovered packages)'
default: ''
type: string
conan_args:
description: 'Conan args'
default: ''
@ -43,8 +51,8 @@ jobs:
if: ${{ inputs.start_builds == true && needs.find-packages.outputs.discovered_packages != '' }}
uses: ultimaker/cura-workflows/.github/workflows/cura-installers.yml@main
with:
cura_conan_version: ${{ needs.find-packages.outputs.cura_package }}
package_overrides: ${{ needs.find-packages.outputs.package_overrides }}
cura_conan_version: ${{ inputs.cura_conan_version != '' && inputs.cura_conan_version || needs.find-packages.outputs.cura_package }}
package_overrides: ${{ needs.find-packages.outputs.package_overrides }} ${{ inputs.package_overrides }}
conan_args: ${{ inputs.conan_args }}
enterprise: ${{ inputs.enterprise }}
staging: ${{ inputs.staging }}

View file

@ -1,17 +1,17 @@
version: "5.11.0"
version: "5.12.0-alpha.0"
commit: "unknown"
requirements:
- "cura_resources/5.11.0"
- "uranium/5.11.0"
- "curaengine/5.11.0"
- "cura_binary_data/5.11.0"
- "fdm_materials/5.11.0"
- "cura_resources/5.12.0-alpha.0@ultimaker/testing"
- "uranium/5.12.0-alpha.0@ultimaker/testing"
- "curaengine/5.12.0-alpha.0@ultimaker/testing"
- "cura_binary_data/5.12.0-alpha.0@ultimaker/testing"
- "fdm_materials/5.12.0-alpha.0@ultimaker/testing"
- "dulcificum/5.10.0"
- "pysavitar/5.11.0-alpha.0"
- "pynest2d/5.10.0"
requirements_internal:
- "fdm_materials/5.11.0"
- "cura_private_data/5.11.0-alpha.0@internal/testing"
- "fdm_materials/5.12.0-alpha.0@ultimaker/testing"
- "cura_private_data/5.12.0-alpha.0@internal/testing"
requirements_enterprise:
- "native_cad_plugin/2.0.0"
urls:

View file

@ -153,71 +153,110 @@ class CuraConan(ConanFile):
def _retrieve_pip_license(self, package, sources_url, dependency_description):
# Download the sources to get the license file inside
self.output.info(f"Retrieving license for {package}")
response = requests.get(sources_url)
response.raise_for_status()
try:
response = requests.get(sources_url)
response.raise_for_status()
with tempfile.TemporaryDirectory() as temp_dir:
sources_path = os.path.join(temp_dir, "sources.tar.gz")
with open(sources_path, 'wb') as sources_file:
sources_file.write(response.content)
with tempfile.TemporaryDirectory() as temp_dir:
sources_path = os.path.join(temp_dir, "sources.tar.gz")
with open(sources_path, 'wb') as sources_file:
sources_file.write(response.content)
with tarfile.open(sources_path, 'r:gz') as sources_archive:
license_file = "LICENSE"
with tarfile.open(sources_path, 'r:gz') as sources_archive:
license_file = "LICENSE"
for source_file in sources_archive.getnames():
if Path(source_file).name == license_file:
sources_archive.extract(source_file, temp_dir)
for source_file in sources_archive.getnames():
if Path(source_file).name == license_file:
sources_archive.extract(source_file, temp_dir)
license_file_path = os.path.join(temp_dir, source_file)
with open(license_file_path, 'r', encoding='utf8') as file:
dependency_description["license_full"] = file.read()
license_file_path = os.path.join(temp_dir, source_file)
with open(license_file_path, 'r', encoding='utf8') as file:
dependency_description["license_full"] = file.read()
break
except Exception as e:
self.output.warning(f"Failed to retrieve license for {package} from {sources_url}: {e}")
# Don't fail the build, just continue without the license
def _make_pip_dependency_description(self, package, version, dependencies):
url = ["https://pypi.org/pypi", package]
if version is not None:
url.append(version)
# Strip local version identifiers (everything after '+') for PyPI API compatibility
# e.g., "1.26.1+mkl" becomes "1.26.1"
clean_version = version.split('+')[0] if '+' in version else version
url.append(clean_version)
url.append("json")
data = requests.get("/".join(url)).json()
try:
response = requests.get("/".join(url))
response.raise_for_status()
data = response.json()
except (requests.RequestException, ValueError) as e:
self.output.warning(f"Failed to retrieve PyPI data for {package}: {e}")
# Create minimal dependency description with fallback values
dependencies[package] = {
"summary": f"Package {package}",
"version": version or "unknown",
"license": "unknown"
}
return
# Check if the response has the expected structure
if "info" not in data:
self.output.warning(f"PyPI response for {package} missing 'info' field")
dependencies[package] = {
"summary": f"Package {package}",
"version": version or "unknown",
"license": "unknown"
}
return
info = data["info"]
dependency_description = {
"summary": data["info"]["summary"],
"version": data["info"]["version"],
"license": data["info"]["license"]
"summary": info.get("summary", f"Package {package}"),
"version": version or info.get("version", "unknown"), # Use original version if available
"license": info.get("license", "unknown")
}
for url_data in data["urls"]:
if url_data["packagetype"] == "sdist":
sources_url = url_data["url"]
dependency_description["sources_url"] = sources_url
# Handle URLs section safely
if "urls" in data:
for url_data in data["urls"]:
if url_data.get("packagetype") == "sdist":
sources_url = url_data.get("url")
if sources_url:
dependency_description["sources_url"] = sources_url
if not self.options.skip_licenses_download:
self._retrieve_pip_license(package, sources_url, dependency_description)
if not self.options.skip_licenses_download:
try:
self._retrieve_pip_license(package, sources_url, dependency_description)
except Exception as e:
self.output.warning(f"Failed to retrieve license for {package}: {e}")
for source_url, check_source in [("source", False),
("Source", False),
("Source Code", False),
("Repository", False),
("Code", False),
("homepage", True),
("Homepage", True)]:
try:
url = data["info"]["project_urls"][source_url]
if check_source and not self._is_repository_url(url):
# That will not work for ALL open-source projects, but should already get a large majority of them
self.output.warning(f"Source URL for {package} ({url}) doesn't seem to be a supported repository")
continue
dependency_description["sources_url"] = url
break
except KeyError:
pass
# Handle project URLs safely
if "project_urls" in info:
for source_url, check_source in [("source", False),
("Source", False),
("Source Code", False),
("Repository", False),
("Code", False),
("homepage", True),
("Homepage", True)]:
try:
url = info["project_urls"][source_url]
if check_source and not self._is_repository_url(url):
# That will not work for ALL open-source projects, but should already get a large majority of them
self.output.warning(f"Source URL for {package} ({url}) doesn't seem to be a supported repository")
continue
dependency_description["sources_url"] = url
break
except (KeyError, TypeError):
pass
if dependency_description["license"] is not None and len(dependency_description["license"]) > 32:
# Some packages have their full license in this field
dependency_description["license_full"] = dependency_description["license"]
dependency_description["license"] = data["info"]["name"]
dependency_description["license"] = info.get("name", package)
dependencies[data["info"]["name"]] = dependency_description
dependencies[info.get("name", package)] = dependency_description
@staticmethod
def _get_license_from_repository(sources_url, version, license_file_name = None):

View file

@ -116,7 +116,8 @@ class BuildVolume(SceneNode):
self._application.engineCreatedSignal.connect(self._onEngineCreated)
self._has_errors = False
self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged)
scene = self._application.getController().getScene()
scene.sceneChanged.connect(self._onSceneChanged)
# Objects loaded at the moment. We are connected to the property changed events of these objects.
self._scene_objects = set() # type: Set[SceneNode]
@ -318,16 +319,6 @@ class BuildVolume(SceneNode):
if node_bounding_box and node_bounding_box.top < 0 and not node.getParent().callDecoration("isGroup"):
node.setOutsideBuildArea(True)
continue
# Mark the node as outside build volume if the set extruder is disabled
extruder_position = node.callDecoration("getActiveExtruderPosition")
try:
if not self._global_container_stack.extruderList[int(extruder_position)].isEnabled and not node.callDecoration("isGroup"):
node.setOutsideBuildArea(True)
continue
except IndexError: # Happens when the extruder list is too short. We're not done building the printer in memory yet.
continue
except TypeError: # Happens when extruder_position is None. This object has no extruder decoration.
continue
node.setOutsideBuildArea(False)
@ -655,7 +646,7 @@ class BuildVolume(SceneNode):
extra_z = retraction_hop
return extra_z
def _onStackChanged(self):
def _onStackChanged(self, *args) -> None:
self._stack_change_timer.start()
def _onStackChangeTimerFinished(self) -> None:

View file

@ -1300,6 +1300,7 @@ class CuraApplication(QtApplication):
qmlRegisterSingletonType(CuraSceneController, "Cura", 1, 0, self.getCuraSceneController, "SceneController")
qmlRegisterSingletonType(ExtruderManager, "Cura", 1, 0, self.getExtruderManager, "ExtruderManager")
qmlRegisterSingletonType(MachineManager, "Cura", 1, 0, self.getMachineManager, "MachineManager")
qmlRegisterSingletonType(MachineErrorChecker, "Cura", 1, 0, self.getMachineErrorChecker, "MachineErrorChecker")
qmlRegisterSingletonType(IntentManager, "Cura", 1, 6, self.getIntentManager, "IntentManager")
qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, self.getSettingInheritanceManager, "SettingInheritanceManager")
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, self.getSimpleModeSettingsManagerWrapper, "SimpleModeSettingsManager")

View file

@ -10,6 +10,7 @@ from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry
from cura.CuraApplication import CuraApplication # To find some resource types.
from cura.Settings.GlobalStack import GlobalStack
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from UM.PackageManager import PackageManager # The class we're extending.
from UM.Resources import Resources # To find storage paths for some resource types.
@ -123,8 +124,7 @@ class CuraPackageManager(PackageManager):
"""
ids = self.getPackageContainerIds(package_id)
container_stacks = self._application.getContainerRegistry().findContainerStacks()
global_stacks = [container_stack for container_stack in container_stacks if isinstance(container_stack, GlobalStack)]
global_stacks = CuraContainerRegistry.getInstance().findGlobalStacks()
machine_with_materials = []
machine_with_qualities = []
for container_id in ids:

View file

@ -1,6 +1,5 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Job import Job # For our background task of loading MachineNodes lazily.
from UM.JobQueue import JobQueue # For our background task of loading MachineNodes lazily.
from UM.Logger import Logger
@ -10,7 +9,7 @@ import cura.CuraApplication # Imported like this to prevent circular dependenci
from cura.Machines.MachineNode import MachineNode
from cura.Settings.GlobalStack import GlobalStack # To listen only to global stacks being added.
from typing import Dict, List, Optional, TYPE_CHECKING
from typing import Dict, List, Optional, Any, Set, TYPE_CHECKING
import time
if TYPE_CHECKING:
@ -78,9 +77,10 @@ class ContainerTree:
def _onStartupFinished(self) -> None:
"""Ran after completely starting up the application."""
currently_added = ContainerRegistry.getInstance().findContainerStacks() # Find all currently added global stacks.
JobQueue.getInstance().add(self._MachineNodeLoadJob(self, currently_added))
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
currently_added_stacks = CuraContainerRegistry.getInstance().findGlobalStacks()
definitions_ids = set([stack.definition.getId() for stack in currently_added_stacks])
JobQueue.getInstance().add(self._MachineNodeLoadJob(self, definitions_ids))
class _MachineNodeMap:
"""Dictionary-like object that contains the machines.
@ -153,17 +153,17 @@ class ContainerTree:
faster.
"""
def __init__(self, tree_root: "ContainerTree", container_stacks: List["ContainerStack"]) -> None:
def __init__(self, tree_root: "ContainerTree", definitions_ids: Set[str]) -> None:
"""Creates a new background task.
:param tree_root: The container tree instance. This cannot be obtained through the singleton static
function since the instance may not yet be constructed completely.
:param container_stacks: All of the stacks to pre-load the container trees for. This needs to be provided
from here because the stacks need to be constructed on the main thread because they are QObject.
:param definitions_ids: The IDs of all the definitions to pre-load the container trees for. This needs to be
provided from here because the stacks need to be constructed on the main thread because they are QObject.
"""
self.tree_root = tree_root
self.container_stacks = container_stacks
self.definitions_ids: Set[str] = definitions_ids
super().__init__()
def run(self) -> None:
@ -172,14 +172,12 @@ class ContainerTree:
The ``JobQueue`` will schedule this on a different thread.
"""
Logger.log("d", "Started background loading of MachineNodes")
for stack in self.container_stacks: # Load all currently-added containers.
if not isinstance(stack, GlobalStack):
continue
for definition_id in self.definitions_ids: # Load all currently-added containers.
# Allow a thread switch after every container.
# Experimentally, sleep(0) didn't allow switching. sleep(0.1) or sleep(0.2) neither.
# We're in no hurry though. Half a second is fine.
time.sleep(0.5)
definition_id = stack.definition.getId()
if not self.tree_root.machines.is_loaded(definition_id):
_ = self.tree_root.machines[definition_id]
Logger.log("d", "All MachineNode loading completed")

View file

@ -6,7 +6,7 @@ import time
from collections import deque
from PyQt6.QtCore import QObject, QTimer, pyqtSignal, pyqtProperty
from typing import Optional, Any, Set
from typing import Optional, Any, Set, List
from UM.Logger import Logger
from UM.Settings.SettingDefinition import SettingDefinition
@ -77,21 +77,21 @@ class MachineErrorChecker(QObject):
def _onMachineChanged(self) -> None:
if self._global_stack:
self._global_stack.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
self._global_stack.propertiesChanged.disconnect(self.startErrorCheckPropertyChanged)
self._global_stack.containersChanged.disconnect(self.startErrorCheck)
for extruder in self._global_stack.extruderList:
extruder.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
extruder.propertiesChanged.disconnect(self.startErrorCheckPropertyChanged)
extruder.containersChanged.disconnect(self.startErrorCheck)
self._global_stack = self._machine_manager.activeMachine
if self._global_stack:
self._global_stack.propertyChanged.connect(self.startErrorCheckPropertyChanged)
self._global_stack.propertiesChanged.connect(self.startErrorCheckPropertyChanged)
self._global_stack.containersChanged.connect(self.startErrorCheck)
for extruder in self._global_stack.extruderList:
extruder.propertyChanged.connect(self.startErrorCheckPropertyChanged)
extruder.propertiesChanged.connect(self.startErrorCheckPropertyChanged)
extruder.containersChanged.connect(self.startErrorCheck)
hasErrorUpdated = pyqtSignal()
@ -106,15 +106,15 @@ class MachineErrorChecker(QObject):
def needToWaitForResult(self) -> bool:
return self._need_to_check or self._check_in_progress
def startErrorCheckPropertyChanged(self, key: str, property_name: str) -> None:
def startErrorCheckPropertyChanged(self, key: str, property_names: List[str]) -> None:
"""Start the error check for property changed
this is separate from the startErrorCheck because it ignores a number property types
:param key:
:param property_name:
:param property_names:
"""
if property_name != "value":
if "validationState" not in property_names and "enabled" not in property_names:
return
self._keys_to_check.add(key)
self.startErrorCheck()
@ -212,7 +212,6 @@ class MachineErrorChecker(QObject):
if result != self._has_errors:
self._has_errors = result
self.hasErrorUpdated.emit()
self._machine_manager.stacksValidationChanged.emit()
self._keys_to_check = keys_to_recheck if keys_to_recheck else set()
self._need_to_check = False
self._check_in_progress = False

View file

@ -34,6 +34,9 @@ class OneAtATimeIterator(Iterator.Iterator):
if getattr(node, "_outside_buildarea", False):
continue
if node.callDecoration("isAssignedToDisabledExtruder"):
continue
if node.callDecoration("getConvexHull"):
node_list.append(node)

View file

@ -71,7 +71,7 @@ class PlatformPhysics:
nodes = list(BreadthFirstIterator(root))
# Only check nodes inside build area.
nodes = [node for node in nodes if (hasattr(node, "_outside_buildarea") and not node._outside_buildarea)]
nodes = [node for node in nodes if (hasattr(node, "_outside_buildarea") and not node._outside_buildarea and not node.callDecoration("isAssignedToDisabledExtruder"))]
# We try to shuffle all the nodes to prevent "locked" situations, where iteration B inverts iteration A.
# By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve.

View file

@ -97,7 +97,8 @@ class PreviewPass(RenderPass):
# Fill up the batch with objects that can be sliced.
for node in DepthFirstIterator(self._root):
if hasattr(node, "_outside_buildarea") and not getattr(node, "_outside_buildarea"):
if (hasattr(node, "_outside_buildarea") and not getattr(node, "_outside_buildarea") and
not node.callDecoration("isAssignedToDisabledExtruder")):
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
per_mesh_stack = node.callDecoration("getStack")
if node.callDecoration("isNonThumbnailVisibleMesh"):

View file

@ -1,9 +1,12 @@
# Copyright (c) 2025 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
import copy
import json
import numpy
from typing import Optional, Dict
from typing import Optional, Dict, List
from PyQt6.QtCore import QBuffer
from PyQt6.QtCore import QBuffer, QTimer
from PyQt6.QtGui import QImage, QImageWriter
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
@ -18,8 +21,18 @@ class SliceableObjectDecorator(SceneNodeDecorator):
self._paint_texture = None
self._texture_data_mapping: Dict[str, tuple[int, int]] = {}
self._is_assigned_to_disabled_extruder: bool = False
from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance()
if application is not None:
application.getMachineManager().extruderChanged.connect(self._updateIsAssignedToDisabledExtruder)
self._painted_extruders: Optional[List[int]] = None
self.paintTextureChanged = Signal()
self._texture_change_timer: Optional[QTimer] = None
def isSliceable(self) -> bool:
return True
@ -29,6 +42,39 @@ class SliceableObjectDecorator(SceneNodeDecorator):
def getPaintTextureChangedSignal(self) -> Signal:
return self.paintTextureChanged
def setPaintedExtrudersCountDirty(self) -> None:
if self._texture_change_timer is None:
# Lazy initialize the timer because constructor can be called from non-Qt thread
self._texture_change_timer = QTimer()
self._texture_change_timer.setInterval(500) # Long interval to avoid triggering during painting
self._texture_change_timer.setSingleShot(True)
self._texture_change_timer.timeout.connect(self._onTextureChangeTimerFinished)
self._texture_change_timer.start()
def _onTextureChangeTimerFinished(self) -> None:
self._painted_extruders = None
if (self._paint_texture is None or self._paint_texture.getImage() is None or
"extruder" not in self._texture_data_mapping):
return
image = self._paint_texture.getImage()
image_bits = image.constBits()
image_bits.setsize(image.sizeInBytes())
image_array = numpy.frombuffer(image_bits, dtype=numpy.uint32)
bit_range_start, bit_range_end = self._texture_data_mapping["extruder"]
full_int32 = 0xffffffff
bit_mask = (((full_int32 << (32 - 1 - (bit_range_end - bit_range_start))) & full_int32) >> (
32 - 1 - bit_range_end))
texel_counts = numpy.bincount((image_array & bit_mask) >> bit_range_start)
self._painted_extruders = [extruder_nr for extruder_nr, count in enumerate(texel_counts) if count > 0]
from cura.CuraApplication import CuraApplication
CuraApplication.getInstance().globalContainerStackChanged.emit()
def setPaintTexture(self, texture: Texture) -> None:
self._paint_texture = texture
self.paintTextureChanged.emit()
@ -63,6 +109,24 @@ class SliceableObjectDecorator(SceneNodeDecorator):
return texture_buffer.data()
def isAssignedToDisabledExtruder(self) -> bool:
return self._is_assigned_to_disabled_extruder
def _updateIsAssignedToDisabledExtruder(self) -> None:
new_is_assigned_to_disabled_extruder = False
try:
extruder_stack = self.getNode().getPrintingExtruder()
new_is_assigned_to_disabled_extruder = ((extruder_stack is None or not extruder_stack.isEnabled) and
not self.getNode().callDecoration("isGroup"))
except IndexError: # Happens when the extruder list is too short. We're not done building the printer in memory yet.
pass
except TypeError: # Happens when extruder_position is None. This object has no extruder decoration.
pass
self._is_assigned_to_disabled_extruder = new_is_assigned_to_disabled_extruder
def getPaintedExtruders(self) -> Optional[List[int]]:
return self._painted_extruders
def __deepcopy__(self, memo) -> "SliceableObjectDecorator":
copied_decorator = SliceableObjectDecorator()
copied_decorator.setPaintTexture(copy.deepcopy(self.getPaintTexture()))

View file

@ -23,8 +23,8 @@ from UM.Resources import Resources
from UM.Util import parseBool
from cura.ReaderWriters.ProfileWriter import ProfileWriter
from . import ExtruderStack
from . import GlobalStack
from cura.Settings import ExtruderStack
from cura.Settings import GlobalStack
import cura.CuraApplication
from cura.Settings.cura_empty_instance_containers import empty_quality_container
@ -52,6 +52,31 @@ class CuraContainerRegistry(ContainerRegistry):
self._database_handlers["quality"] = QualityDatabaseHandler()
self._database_handlers["intent"] = IntentDatabaseHandler()
def findGlobalStacks(self, **kwargs: Any) -> List[GlobalStack.GlobalStack]:
"""Find all GlobalStack objects matching certain criteria.
:param kwargs: A dictionary of keyword arguments containing
keys and values that need to match the metadata of the GlobalStack.
An asterisk in the values can be used to denote a wildcard.
:note There are currently a lot of calls to findContainerStacks with a type="machine" filter, which technically
returns the very same result, except that the result has to be explicitly converted to GlobalStack class and
is slightly less self-explicit. They can be converted by calling this method instead.
"""
return cast(List[GlobalStack.GlobalStack], self.findContainers(container_type = GlobalStack.GlobalStack, **kwargs))
def findGlobalStacksMetadata(self, **kwargs: Any) -> List[Dict[str, Any]]:
"""Find the metadata of all global stacks matching certain criteria.
:param kwargs: A dictionary of keyword arguments containing keys and
values that need to match the metadata. An asterisk in the values can be
used to denote a wildcard.
:return: A list of metadata dictionaries matching the search criteria, or
an empty list if nothing was found.
"""
return self.findContainersMetadata(container_type = GlobalStack.GlobalStack, **kwargs)
@override(ContainerRegistry)
def addContainer(self, container: ContainerInterface) -> bool:
"""Overridden from ContainerRegistry

View file

@ -1,4 +1,4 @@
# Copyright (c) 2022 Ultimaker B.V.
# Copyright (c) 2025 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt6.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt.
@ -248,11 +248,19 @@ class ExtruderManager(QObject):
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 |= parseBool(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 |= parseBool(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 |= parseBool(stack_to_use.getProperty("support_roof_enable", "value"))
painted_extruders = node.callDecoration("getPaintedExtruders")
if painted_extruders is not None:
for extruder_nr in painted_extruders:
try:
used_extruder_stack_ids.add(self.extruderIds[str(extruder_nr)])
except KeyError:
pass
# Check limit to extruders
limit_to_extruder_feature_list = ["wall_0_extruder_nr",

View file

@ -9,6 +9,7 @@ from typing import Any, List, Dict, TYPE_CHECKING, Optional, cast, Set
from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal, QTimer
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from UM.Decorators import deprecated
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.Interfaces import ContainerInterface
@ -87,8 +88,6 @@ class MachineManager(QObject):
self.globalContainerChanged.connect(self.activeQualityChangesGroupChanged)
self.globalContainerChanged.connect(self.activeQualityGroupChanged)
self._stacks_have_errors = None # type: Optional[bool]
extruder_manager = self._application.getExtruderManager()
extruder_manager.activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
@ -447,31 +446,6 @@ class MachineManager(QObject):
return False
return True
def _checkStacksHaveErrors(self) -> bool:
time_start = time.time()
if self._global_container_stack is None: #No active machine.
return False
if self._global_container_stack.hasErrors():
Logger.log("d", "Checking global stack for errors took %0.2f s and we found an error" % (time.time() - time_start))
return True
# Not a very pretty solution, but the extruder manager doesn't really know how many extruders there are
machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
extruder_stacks = self._global_container_stack.extruderList
count = 1 # We start with the global stack
for stack in extruder_stacks:
md = stack.getMetaData()
if "position" in md and int(md["position"]) >= machine_extruder_count:
continue
count += 1
if stack.hasErrors():
Logger.log("d", "Checking %s stacks for errors took %.2f s and we found an error in stack [%s]" % (count, time.time() - time_start, str(stack)))
return True
Logger.log("d", "Checking %s stacks for errors took %.2f s" % (count, time.time() - time_start))
return False
@pyqtProperty(bool, notify = numUserSettingsChanged)
def hasUserSettings(self) -> bool:
return self._num_user_settings != 0
@ -515,13 +489,10 @@ class MachineManager(QObject):
container.sendPostponedEmits()
@pyqtProperty(bool, notify = stacksValidationChanged)
@deprecated("This property was already inactive and will now be removed, use MachineErrorChecker.hasError instead.", since="5.12.0")
def stacksHaveErrors(self) -> bool:
"""Check if none of the stacks contain error states
Note that the _stacks_have_errors is cached due to performance issues
Calling _checkStack(s)ForErrors on every change is simply too expensive
"""
return bool(self._stacks_have_errors)
"""Check if none of the stacks contain error states"""
return False
@pyqtProperty(str, notify = globalContainerChanged)
def activeMachineFirmwareVersion(self) -> str:

View file

@ -124,7 +124,7 @@ class Snapshot:
def isNodeRenderable(node):
return not getattr(node, "_outside_buildarea", False) and node.callDecoration(
"isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration(
"isNonThumbnailVisibleMesh")
"isNonThumbnailVisibleMesh") and not node.callDecoration("isAssignedToDisabledExtruder")
@staticmethod
def nodeBounds(root_node: SceneNode) -> Optional[AxisAlignedBox]:

View file

@ -48,6 +48,8 @@ class PrintInformation(QObject):
self._pre_sliced = False
self._user_time_estimation_adjusted = False
self._backend = self._application.getBackend()
if self._backend:
self._backend.printDurationMessage.connect(self._onPrintDurationMessage)
@ -128,6 +130,12 @@ class PrintInformation(QObject):
def preSliced(self) -> bool:
return self._pre_sliced
userTimeAdjustedChanged = pyqtSignal()
@pyqtProperty(bool, notify=userTimeAdjustedChanged)
def userTimeAdjusted(self) -> bool:
return self._user_time_estimation_adjusted
def setPreSliced(self, pre_sliced: bool) -> None:
if self._pre_sliced != pre_sliced:
self._pre_sliced = pre_sliced
@ -166,6 +174,47 @@ class PrintInformation(QObject):
def printTimes(self) -> Dict[str, Duration]:
return self._print_times_per_feature[self._active_build_plate]
def _getTimeEstimationFactor(self) -> Optional[float]:
"""Returns the time estimation factor as set by the user in the machine settings, or None if not set.
If a factor is found and differs from 1.0 (100%), the userTimeAdjusted property is set to True.
This factor is used to adjust the estimated print time received from the backend.
Note that the factor is converted from percentage to decimal (e.g., 80% becomes 0.8).
"""
global_stack = self._application.getGlobalContainerStack()
if global_stack is None:
self._resetUserTimeAdjustment()
return None
factor_value = global_stack.getProperty("machine_time_estimation_factor", "value")
if factor_value is None:
self._resetUserTimeAdjustment()
return None
try:
factor = float(factor_value) / 100.0
except (ValueError, TypeError):
Logger.warning(f"Invalid time estimation factor value: {factor_value}")
self._resetUserTimeAdjustment()
return None
# Check if factor is significantly different from 1.0 to avoid floating point precision issues
if abs(factor - 1.0) > 1e-6:
self._setUserTimeAdjustment(True)
return factor
self._resetUserTimeAdjustment()
return None
def _setUserTimeAdjustment(self, adjusted: bool) -> None:
"""Sets the user time adjustment state and emits signal if changed."""
if self._user_time_estimation_adjusted != adjusted:
self._user_time_estimation_adjusted = adjusted
self.userTimeAdjustedChanged.emit()
def _resetUserTimeAdjustment(self) -> None:
"""Resets the user time adjustment state to False."""
self._setUserTimeAdjustment(False)
def _onPrintDurationMessage(self, build_plate_number: int, print_times_per_feature: Dict[str, int], material_amounts: List[float]) -> None:
self._updateTotalPrintTimePerFeature(build_plate_number, print_times_per_feature)
self.currentPrintTimeChanged.emit()
@ -175,6 +224,7 @@ class PrintInformation(QObject):
def _updateTotalPrintTimePerFeature(self, build_plate_number: int, print_times_per_feature: Dict[str, int]) -> None:
total_estimated_time = 0
time_estimation_factor = self._getTimeEstimationFactor()
if build_plate_number not in self._print_times_per_feature:
self._initPrintTimesPerFeature(build_plate_number)
@ -189,6 +239,9 @@ class PrintInformation(QObject):
Logger.warning("Received NaN for print duration message")
continue
if time_estimation_factor is not None:
time = int(time * time_estimation_factor)
total_estimated_time += time
duration.setDuration(time)

28
doc/settings_stacks.puml Normal file
View file

@ -0,0 +1,28 @@
@startuml
ExtruderStack -up-|> CuraContainerStack
CuraContainerStack -up-|> ContainerStack
ContainerStack -up-|> ContainerInterface
ContainerStack *-up- "*" ContainerInterface : _containers
ContainerStack *-up- "1" ContainerInterface : _next_stack
class ContainerStack {
<<signal>> propertyChanged
<<signal>> propertiesChanged
}
DefinitionContainer -up-|> DefinitionContainerInterface
DefinitionContainerInterface -up-|> ContainerInterface
InstanceContainer -up-|> ContainerInterface
InstanceContainer *-- "1" DefinitionContainerInterface
InstanceContainer *-- "*" SettingInstance
SettingInstance o-- "1" SettingDefinition
SettingInstance o-- "1" ContainerInterface
DefinitionContainer *-- "*" SettingDefinition
@enduml

View file

@ -157,7 +157,7 @@ class RestoreBackupJob(Job):
Logger.info("All packages redownloaded!")
self._job_done.set()
else:
msgs = "\n - ".join(redownload_errors)
msgs = "\n".join([" - " + str(x) for x in redownload_errors])
Logger.error(f"Couldn't re-install at least one package(s) because: {msgs}")
self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE
self._job_done.set()

View file

@ -160,6 +160,7 @@ class CuraEngineBackend(QObject, Backend):
self._backend_log_max_lines: int = 20000 # Maximum number of lines to buffer
self._error_message: Optional[Message] = None # Pop-up message that shows errors.
self._unused_extruders: list[int] = [] # Extruder numbers of found unused extruders
self._required_extruders: list[int] = [] # Extruder numbers of found required but disabled extruders
# Count number of objects to see if there is something changed
self._last_num_objects: Dict[int, int] = defaultdict(int)
@ -558,11 +559,25 @@ class CuraEngineBackend(QObject, Backend):
self.setState(BackendState.NotStarted)
if job.getResult() == StartJobResult.ObjectsWithDisabledExtruder:
self._error_message = Message(catalog.i18nc("@info:status",
"Unable to slice because there are objects associated with disabled Extruder %s.") % job.getAssociatedDisabledExtruders(),
title = catalog.i18nc("@info:title", "Unable to slice"),
message_type = Message.MessageType.WARNING)
self._required_extruders = job.getAssociatedDisabledExtruders()
extruder_names = [self._global_container_stack.extruderList[int(idx)].definition.getName() for idx in
self._required_extruders]
disabled_extruders = [f"<li>{extruder_name}</li>" for extruder_name in extruder_names]
self._error_message = Message(
text=catalog.i18nc("@message", "<html>Unable to slice because there are objects associated with at least one disabled extruder:"
f"<ul><b>{"".join(disabled_extruders)}</b></ul></html>"),
title=catalog.i18nc("@info:title", "Unable to slice"),
message_type=Message.MessageType.WARNING
)
self._error_message.addAction("enable_extruders",
name=catalog.i18nc("@button", "Enable required extruder(s)"),
icon="",
description=catalog.i18nc("@label",
"Automatically enable the required extruder(s)")
)
self._error_message.actionTriggered.connect(self._onMessageActionTriggered)
self._error_message.show()
self.setState(BackendState.Error)
self.backendError.emit(job)
return
@ -571,7 +586,6 @@ class CuraEngineBackend(QObject, Backend):
if application.platformActivity:
self._error_message = Message(catalog.i18nc("@info:status", "Please review settings and check if your models:"
"\n- Fit within the build volume"
"\n- Are assigned to an enabled extruder"
"\n- Are not all set as modifier meshes"),
title = catalog.i18nc("@info:title", "Unable to slice"),
message_type = Message.MessageType.WARNING)
@ -999,6 +1013,10 @@ class CuraEngineBackend(QObject, Backend):
message.hide()
for unused_extruder in self._unused_extruders:
CuraApplication.getInstance().getMachineManager().setExtruderEnabled(unused_extruder, False)
elif message_action == "enable_extruders":
message.hide()
for required_extruder in self._required_extruders:
CuraApplication.getInstance().getMachineManager().setExtruderEnabled(required_extruder, True)
def _parseMessagePrintTimes(self, message: Arcus.PythonMessage) -> Dict[str, float]:
"""Called for parsing message to retrieve estimated time per feature

View file

@ -234,7 +234,7 @@ class StartSliceJob(Job):
self._slice_message: Arcus.PythonMessage = slice_message
self._is_cancelled: bool = False
self._build_plate_number: Optional[int] = None
self._associated_disabled_extruders: Optional[str] = None
self._associated_disabled_extruders: List[int] = []
# cache for all setting values from all stacks (global & extruder) for the current machine
self._all_extruders_settings: Optional[Dict[str, Any]] = None
@ -242,7 +242,7 @@ class StartSliceJob(Job):
def getSliceMessage(self) -> Arcus.PythonMessage:
return self._slice_message
def getAssociatedDisabledExtruders(self) -> Optional[str]:
def getAssociatedDisabledExtruders(self) -> List[int]:
return self._associated_disabled_extruders
def setBuildPlate(self, build_plate_number: int) -> None:
@ -296,11 +296,6 @@ class StartSliceJob(Job):
self.setResult(StartJobResult.Error)
return
# Don't slice if there is a setting with an error value.
if CuraApplication.getInstance().getMachineManager().stacksHaveErrors:
self.setResult(StartJobResult.SettingError)
return
if CuraApplication.getInstance().getBuildVolume().hasErrors():
self.setResult(StartJobResult.BuildPlateError)
return
@ -309,6 +304,7 @@ class StartSliceJob(Job):
while CuraApplication.getInstance().getMachineErrorChecker().needToWaitForResult:
time.sleep(0.1)
# Don't slice if there is a setting with an error value.
if CuraApplication.getInstance().getMachineErrorChecker().hasError:
self.setResult(StartJobResult.SettingError)
return
@ -416,7 +412,7 @@ class StartSliceJob(Job):
# Only check if the printing extruder is enabled for printing meshes
is_non_printing_mesh = node.callDecoration("evaluateIsNonPrintingMesh")
if not is_non_printing_mesh:
for used_extruder in StartSliceJob._getUsedExtruders(node):
for used_extruder in StartSliceJob._getMainExtruders(node):
if not extruders_enabled[used_extruder]:
skip_group = True
has_model_with_disabled_extruders = True
@ -426,8 +422,7 @@ class StartSliceJob(Job):
if has_model_with_disabled_extruders:
self.setResult(StartJobResult.ObjectsWithDisabledExtruder)
associated_disabled_extruders = {p + 1 for p in associated_disabled_extruders}
self._associated_disabled_extruders = ", ".join(map(str, sorted(associated_disabled_extruders)))
self._associated_disabled_extruders = sorted(list(associated_disabled_extruders))
return
# There are cases when there is nothing to slice. This can happen due to one at a time slicing not being
@ -763,28 +758,11 @@ class StartSliceJob(Job):
self._addRelations(relations_set, relation.target.relations)
@staticmethod
def _getUsedExtruders(node: SceneNode) -> List[int]:
used_extruders = []
# Look at extruders used in painted texture
node_texture = node.callDecoration("getPaintTexture")
texture_data_mapping = node.callDecoration("getTextureDataMapping")
if node_texture is not None and texture_data_mapping is not None and "extruder" in texture_data_mapping:
texture_image = node_texture.getImage()
image_ptr = texture_image.constBits()
image_ptr.setsize(texture_image.sizeInBytes())
image_size = texture_image.height(), texture_image.width()
image_array = numpy.frombuffer(image_ptr, dtype=numpy.uint32).reshape(image_size)
bit_range_start, bit_range_end = texture_data_mapping["extruder"]
full_int32 = 0xffffffff
bit_mask = (((full_int32 << (32 - 1 - (bit_range_end - bit_range_start))) & full_int32) >> (
32 - 1 - bit_range_end))
used_extruders = (numpy.unique(image_array & bit_mask) >> bit_range_start).tolist()
def _getMainExtruders(node: SceneNode) -> List[int]:
used_extruders = node.callDecoration("getPaintedExtruders")
# There is no relevant painting data, just take the extruder associated to the model
if not used_extruders:
used_extruders = [int(node.callDecoration("getActiveExtruderPosition"))]
return used_extruders
return used_extruders

View file

@ -167,6 +167,23 @@ Item
// This has something to do with UM2 and UM2+ regarding "has_material" and the gcode flavor settings.
// I don't remember exactly what.
afterOnEditingFinishedFunction: manager.updateHasMaterialsMetadata
}
Cura.NumericTextFieldWithUnit // "Print Time Estimation Factor"
{
id: machineTimeEstimationFactorField
containerStackId: machineStackId
settingKey: "machine_time_estimation_factor"
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "Print Time Estimation Factor")
labelFont: base.labelFont
labelWidth: base.labelWidth
controlWidth: base.controlWidth
unitText: catalog.i18nc("@label", "%")
decimals: 1
minimum: 1
maximum: 1000
forceUpdateOnChangeFunction: forceUpdateFunction
}
}

View file

@ -188,7 +188,7 @@ class MakerbotWriter(MeshWriter):
if not getattr(node, "_outside_buildarea", False):
if node.callDecoration(
"isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration(
"isNonThumbnailVisibleMesh"):
"isNonThumbnailVisibleMesh") and not node.callDecoration("isAssignedToDisabledExtruder"):
nodes.append(node)
meta = dict()

View file

@ -109,4 +109,6 @@ class MultiMaterialExtruderConverter:
texture.updateImagePart(image.rect())
node.callDecoration("setPaintedExtrudersCountDirty")
self.mainExtruderChanged.emit(node)

View file

@ -5,6 +5,7 @@ from typing import Optional
from PyQt6.QtGui import QUndoCommand, QImage, QPainter, QBrush
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from UM.View.GL.Texture import Texture
from .PaintCommand import PaintCommand
@ -13,8 +14,12 @@ from .PaintCommand import PaintCommand
class PaintClearCommand(PaintCommand):
"""Provides the command that clears all the painting for the current mode"""
def __init__(self, texture: Texture, bit_range: tuple[int, int], set_value: int) -> None:
super().__init__(texture, bit_range)
def __init__(self,
texture: Texture,
bit_range: tuple[int, int],
set_value: int,
sliceable_object_decorator: Optional[SliceableObjectDecorator] = None) -> None:
super().__init__(texture, bit_range, sliceable_object_decorator=sliceable_object_decorator)
self._set_value = set_value
def id(self) -> int:
@ -22,13 +27,12 @@ class PaintClearCommand(PaintCommand):
def redo(self) -> None:
painter = self._makeClearedTexture()
if self._set_value > 0:
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceOrDestination)
painter.fillRect(self._texture.getImage().rect(), QBrush(self._set_value))
painter.end()
self._setPaintedExtrudersCountDirty()
self._texture.updateImagePart(self._bounding_rect)
def mergeWith(self, command: QUndoCommand) -> bool:
@ -38,6 +42,6 @@ class PaintClearCommand(PaintCommand):
# There is actually nothing more to do here, both clear commands already have the same original texture
return True
def _clearTextureBits(self, painter: QPainter):
def _clearTextureBits(self, painter: QPainter, extended = False):
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_NotSourceAndDestination)
painter.fillRect(self._texture.getImage().rect(), QBrush(self._getBitRangeMask()))

View file

@ -1,12 +1,14 @@
# Copyright (c) 2025 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Tuple, Optional
from typing import Tuple, Optional, Dict
from PyQt6.QtCore import QRect
import numpy
from PyQt6.QtGui import QUndoCommand, QImage, QPainter, QBrush
from UM.View.GL.Texture import Texture
from cura.CuraApplication import CuraApplication
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
class PaintCommand(QUndoCommand):
@ -14,7 +16,11 @@ class PaintCommand(QUndoCommand):
FULL_INT32 = 0xffffffff
def __init__(self, texture: Texture, bit_range: tuple[int, int], make_original_image = True) -> None:
def __init__(self,
texture: Texture,
bit_range: tuple[int, int],
make_original_image = True,
sliceable_object_decorator: Optional[SliceableObjectDecorator] = None) -> None:
super().__init__()
self._texture: Texture = texture
@ -22,6 +28,8 @@ class PaintCommand(QUndoCommand):
self._original_texture_image = None
self._bounding_rect = texture.getImage().rect()
self._sliceable_object_decorator: Optional[SliceableObjectDecorator] = sliceable_object_decorator
if make_original_image:
self._original_texture_image = self._texture.getImage().copy()
painter = QPainter(self._original_texture_image)
@ -35,21 +43,26 @@ class PaintCommand(QUndoCommand):
if self._original_texture_image is None:
return
painter = self._makeClearedTexture()
painter = self._makeClearedTexture(extended=True)
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceOrDestination)
painter.drawImage(0, 0, self._original_texture_image)
painter.end()
self._setPaintedExtrudersCountDirty()
self._texture.updateImagePart(self._bounding_rect)
def _makeClearedTexture(self) -> QPainter:
def _setPaintedExtrudersCountDirty(self) -> None:
if self._sliceable_object_decorator is not None:
self._sliceable_object_decorator.setPaintedExtrudersCountDirty()
def _makeClearedTexture(self, extended = False) -> QPainter:
painter = QPainter(self._texture.getImage())
painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
self._clearTextureBits(painter)
self._clearTextureBits(painter, extended)
return painter
def _clearTextureBits(self, painter: QPainter):
def _clearTextureBits(self, painter: QPainter, extended = False):
raise NotImplementedError()
@staticmethod

View file

@ -7,6 +7,7 @@ import math
from PyQt6.QtCore import QRect, QRectF, QPoint
from PyQt6.QtGui import QUndoCommand, QImage, QPainter, QPainterPath, QPen, QBrush
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from UM.View.GL.Texture import Texture
from UM.Math.Polygon import Polygon
@ -16,14 +17,16 @@ class PaintStrokeCommand(PaintCommand):
"""Provides the command that does the actual painting on objects with undo/redo mechanisms"""
PEN_OVERLAP_WIDTH = 2.5
PEN_OVERLAP_WIDTH_EXTENDED = PEN_OVERLAP_WIDTH + 0.5
def __init__(self,
texture: Texture,
stroke_polygons: List[Polygon],
set_value: int,
bit_range: tuple[int, int],
mergeable: bool) -> None:
super().__init__(texture, bit_range, make_original_image = not mergeable)
mergeable: bool,
sliceable_object_decorator: Optional[SliceableObjectDecorator] = None) -> None:
super().__init__(texture, bit_range, make_original_image = not mergeable, sliceable_object_decorator=sliceable_object_decorator)
self._stroke_polygons: List[Polygon] = stroke_polygons
self._calculateBoundingRect()
self._set_value: int = set_value
@ -40,6 +43,7 @@ class PaintStrokeCommand(PaintCommand):
painter.drawPath(self._makePainterPath())
painter.end()
self._setPaintedExtrudersCountDirty()
self._texture.updateImagePart(self._bounding_rect)
def mergeWith(self, command: QUndoCommand) -> bool:
@ -55,9 +59,9 @@ class PaintStrokeCommand(PaintCommand):
return True
def _clearTextureBits(self, painter: QPainter):
def _clearTextureBits(self, painter: QPainter, extended = False):
painter.setBrush(QBrush(self._getBitRangeMask()))
painter.setPen(QPen(painter.brush(), self.PEN_OVERLAP_WIDTH))
painter.setPen(QPen(painter.brush(), self.PEN_OVERLAP_WIDTH_EXTENDED if extended else self.PEN_OVERLAP_WIDTH))
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_NotSourceAndDestination)
painter.drawPath(self._makePainterPath())

View file

@ -412,7 +412,7 @@ class PaintTool(Tool):
Logger.logException("e", "Error when adding paint stroke")
self._last_world_coords = world_coords
self._updateScene(painted_object, update_node = self._mouse_held)
self._updateScene(painted_object, update_node = event_caught)
return event_caught
return False

View file

@ -326,7 +326,7 @@ Item
UM.Label
{
anchors.fill: parent
text: catalog.i18nc("@label", "Select a single model to start painting")
text: catalog.i18nc("@label", "Select a single ungrouped model to start painting")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}

View file

@ -23,6 +23,7 @@ from UM.View.GL.OpenGL import OpenGL
from UM.i18n import i18nCatalog
from UM.Math.Color import Color
from UM.Math.Polygon import Polygon
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from .PaintStrokeCommand import PaintStrokeCommand
from .PaintClearCommand import PaintClearCommand
@ -242,7 +243,14 @@ class PaintView(CuraView):
stroke_path,
set_value,
self._current_bits_ranges,
merge_with_previous))
merge_with_previous,
self._getSliceableObjectDecorator()))
def _getSliceableObjectDecorator(self) -> Optional[SliceableObjectDecorator]:
if self._painted_object is None or self._current_paint_type != "extruder":
return None
return self._painted_object.getDecorator(SliceableObjectDecorator)
def _makeClearCommand(self) -> Optional[PaintClearCommand]:
if self._painted_object is None or self._paint_texture is None or self._current_bits_ranges is None:
@ -254,7 +262,10 @@ class PaintView(CuraView):
if extruder_stack is not None:
set_value = extruder_stack.getValue("extruder_nr")
return PaintClearCommand(self._paint_texture, self._current_bits_ranges, set_value)
return PaintClearCommand(self._paint_texture,
self._current_bits_ranges,
self._shiftTextureValue(set_value),
self._getSliceableObjectDecorator())
def clearPaint(self):
self._prepareDataMapping()

View file

@ -91,7 +91,7 @@ class AnnealingOrDrying(Script):
"description": "Hold the bed temp at the 'Bed Start Out Temperature' for this amount of time (in decimal hours). When this time expires then the Annealing cool down will start. This is also the 'Drying Time' used when 'Drying Filament'.",
"type": "float",
"default_value": 0.0,
"unit": "Decimal Hrs ",
"unit": "Hrs ",
"enabled": "enable_script and cycle_type == 'anneal_cycle'"
},
"dry_time":
@ -100,7 +100,7 @@ class AnnealingOrDrying(Script):
"description": "Hold the bed temp at the 'Bed Start Out Temperature' for this amount of time (in decimal hours). When this time expires the bed will shut off.",
"type": "float",
"default_value": 4.0,
"unit": "Decimal Hrs ",
"unit": "Hrs ",
"enabled": "enable_script and cycle_type == 'dry_cycle'"
},
"pause_cmd":
@ -117,7 +117,7 @@ class AnnealingOrDrying(Script):
"description": "Enter the temperature to start at. This is typically the bed temperature during the print but can be changed here. This is also the temperature used when drying filament.",
"type": "int",
"value": 30,
"unit": "Degrees ",
"unit": "°C/F ",
"minimum_value": 30,
"maximum_value": 110,
"maximum_value_warning": 100,
@ -129,7 +129,7 @@ class AnnealingOrDrying(Script):
"description": "Enter the lowest temperature to control the cool down. This is the shut-off temperature for the build plate and (when applicable) the Heated Chamber. The minimum value is 30",
"type": "int",
"default_value": 30,
"unit": "Degrees ",
"unit": "°C/F ",
"minimum_value": 30,
"enabled": "enable_script and cycle_type == 'anneal_cycle'"
},
@ -139,7 +139,7 @@ class AnnealingOrDrying(Script):
"description": "Enter the temperature for the Build Volume (Heated Chamber). This is typically the temperature during the print but can be changed here.",
"type": "int",
"value": 24,
"unit": "Degrees ",
"unit": "°C/F ",
"minimum_value": 0,
"maximum_value": 90,
"maximum_value_warning": 75,
@ -170,7 +170,7 @@ class AnnealingOrDrying(Script):
"description": "The total amount of time (in decimal hours) to control the cool down. The build plate temperature will be dropped in 3° increments across this time span. 'Cool Down Time' starts at the end of the 'Hold Time' if you entered one.",
"type": "float",
"default_value": 1.0,
"unit": "Decimal Hrs ",
"unit": "Hrs ",
"minimum_value_warning": 0.25,
"enabled": "enable_script and cycle_type == 'anneal_cycle'"
},
@ -203,7 +203,7 @@ class AnnealingOrDrying(Script):
"label": "Beep Duration",
"description": "The length of the buzzer sound. Units are in milliseconds so 1000ms = 1 second.",
"type": "int",
"unit": "milliseconds ",
"unit": "msec ",
"default_value": 1000,
"enabled": "beep_when_done and enable_script"
},
@ -471,9 +471,9 @@ class AnnealingOrDrying(Script):
def _dry_filament_only(
self,
drydata: str,
bed_temperature: int,
chamber_temp: int,
drydata: str,
heated_chamber: bool,
heating_zone: str,
max_y: str,
@ -562,10 +562,10 @@ class AnnealingOrDrying(Script):
back_txt = lines[index].split(";")[1]
lines[index] = front_txt + str(" " * (30 - len(front_txt))) +";" + back_txt
drydata[1] = "\n".join(lines) + "\n"
dry_txt = "; Drying time ...................... " + str(self.getSettingValueByKey("dry_time")) + " hrs\n"
dry_txt = "; Drying time ............... " + str(self.getSettingValueByKey("dry_time")) + " hrs\n"
dry_txt += "; Drying temperature ........ " + str(bed_temperature) + "°\n"
if heated_chamber and heating_zone == "bed_chamber":
dry_txt += "; Chamber temperature ... " + str(chamber_temp) + "°\n"
dry_txt += "; Chamber temperature ....... " + str(chamber_temp) + "°\n"
Message(title = "[Dry Filament]", text = dry_txt).show()
drydata[0] = "; <<< This is a filament drying file only. There is no actual print. >>>\n;\n" + dry_txt + ";\n"
return drydata

View file

@ -486,9 +486,9 @@ class PurgeLinesAndUnload(Script):
data[0] += "; [Purge Lines and Unload] 'Add Purge Lines' did not run because the assumed primary nozzle (T0) has tool offsets.\n"
Message(title = "[Purge Lines and Unload]", text = "'Add Purge Lines' did not run because the assumed primary nozzle (T0) has tool offsets").show()
return data
self.purge_line_hgt = round(float(self.global_stack.getProperty("layer_height_0", "value")),2)
def calculate_purge_volume(line_width, purge_length, volume_per_mm):
return round((line_width * 0.3 * purge_length) * 1.25 / volume_per_mm, 5)
return round((line_width * self.purge_line_hgt * purge_length) * 1.25 / volume_per_mm, 5)
def adjust_for_prime_blob_gcode(retract_speed, retract_distance):
"""Generates G-code lines for prime blob adjustment."""
@ -500,7 +500,7 @@ class PurgeLinesAndUnload(Script):
purge_location = self.getSettingValueByKey("purge_line_location")
purge_extrusion_full = True if self.getSettingValueByKey("purge_line_length") == "purge_full" else False
purge_str = ";TYPE:CUSTOM----------[Purge Lines]\nG0 F600 Z2 ; Move up\nG92 E0 ; Reset extruder\n"
purge_str = ";---------------------[Purge Lines]\nG0 F600 Z2 ; Move up\nG92 E0 ; Reset extruder\n"
purge_str += self._get_blob_code()
# Normal cartesian printer with origin at the left front corner
if self.bed_shape == "rectangular" and not self.origin_at_center:
@ -511,7 +511,7 @@ class PurgeLinesAndUnload(Script):
purge_str = purge_str.replace("Lines", "Lines at MinX")
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} X{self.machine_left + self.border_distance} Y{self.machine_front + 10} ; Move to start\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
purge_str += f"G0 F600 Z{self.purge_line_hgt} ; Move down\n"
if self.prime_blob_enable:
purge_str += adjust_for_prime_blob_gcode(self.retract_speed, self.retract_dist)
# Purge two lines
@ -522,7 +522,7 @@ class PurgeLinesAndUnload(Script):
purge_str += f"G1 F{int(self.retract_speed)} E{round(purge_volume * 2 - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z8 ; Move Up\nG4 S1 ; Wait for 1 second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} X{self.machine_left + 3 + self.border_distance} Y{self.machine_front + 20} Z0.3 ; Slide over and down\n"
purge_str += f"G0 F{self.print_speed} X{self.machine_left + 3 + self.border_distance} Y{self.machine_front + 20} Z{self.purge_line_hgt} ; Slide over and down\n"
purge_str += f"G0 X{self.machine_left + 3 + self.border_distance} Y{self.machine_front + 35} ; Wipe\n"
self.end_purge_location = Position.LEFT_FRONT
elif purge_location == Location.RIGHT:
@ -532,7 +532,7 @@ class PurgeLinesAndUnload(Script):
purge_str = purge_str.replace("Lines", "Lines at MaxX")
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} X{self.machine_right - self.border_distance} ; Move\nG0 Y{self.machine_back - 10} ; Move\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
purge_str += f"G0 F600 Z{self.purge_line_hgt} ; Move down\n"
if self.prime_blob_enable:
purge_str += adjust_for_prime_blob_gcode(self.retract_speed, self.retract_dist)
# Purge two lines
@ -543,7 +543,7 @@ class PurgeLinesAndUnload(Script):
purge_str += f"G1 F{int(self.retract_speed)} E{round(purge_volume * 2 - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z8 ; Move Up\nG4 S1 ; Wait for 1 second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} X{self.machine_right - 3 - self.border_distance} Y{self.machine_back - 20} Z0.3 ; Slide over and down\n"
purge_str += f"G0 F{self.print_speed} X{self.machine_right - 3 - self.border_distance} Y{self.machine_back - 20} Z{self.purge_line_hgt} ; Slide over and down\n"
purge_str += f"G0 X{self.machine_right - 3 - self.border_distance} Y{self.machine_back - 35} ; Wipe\n"
self.end_purge_location = Position.RIGHT_REAR
elif purge_location == Location.FRONT:
@ -554,7 +554,7 @@ class PurgeLinesAndUnload(Script):
purge_str = purge_str.replace("Lines", "Lines at MinY")
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} X{self.machine_left + 10} Y{self.machine_front + self.border_distance} ; Move to start\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
purge_str += f"G0 F600 Z{self.purge_line_hgt} ; Move down\n"
if self.prime_blob_enable:
purge_str += adjust_for_prime_blob_gcode(self.retract_speed, self.retract_dist)
# Purge two lines
@ -565,7 +565,7 @@ class PurgeLinesAndUnload(Script):
purge_str += f"G1 F{int(self.retract_speed)} E{round(purge_volume * 2 - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z8 ; Move Up\nG4 S1 ; Wait for 1 second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} X{self.machine_left + 20} Y{self.machine_front + 3 + self.border_distance} Z0.3 ; Slide over and down\n"
purge_str += f"G0 F{self.print_speed} X{self.machine_left + 20} Y{self.machine_front + 3 + self.border_distance} Z{self.purge_line_hgt} ; Slide over and down\n"
purge_str += f"G0 X{self.machine_left + 35} Y{self.machine_front + 3 + self.border_distance} ; Wipe\n"
self.end_purge_location = Position.LEFT_FRONT
elif purge_location == Location.REAR:
@ -577,7 +577,7 @@ class PurgeLinesAndUnload(Script):
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} Y{self.machine_back - self.border_distance} ; Ortho Move to back\n"
purge_str += f"G0 X{self.machine_right - 10} ; Ortho move to start\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
purge_str += f"G0 F600 Z{self.purge_line_hgt} ; Move down\n"
if self.prime_blob_enable:
purge_str += adjust_for_prime_blob_gcode(self.retract_speed, self.retract_dist)
# Purge two lines
@ -588,7 +588,7 @@ class PurgeLinesAndUnload(Script):
purge_str += f"G1 F{int(self.retract_speed)} E{round(purge_volume * 2 - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z8 ; Move Up\nG4 S1 ; Wait 1 second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} X{self.machine_right - 20} Y{self.machine_back - 3 - self.border_distance} Z0.3 ; Slide over and down\n"
purge_str += f"G0 F{self.print_speed} X{self.machine_right - 20} Y{self.machine_back - 3 - self.border_distance} Z{self.purge_line_hgt} ; Slide over and down\n"
purge_str += f"G0 X{self.machine_right - 35} Y{self.machine_back - 3 - self.border_distance} ; Wipe\n"
self.end_purge_location = Position.RIGHT_REAR
# Some cartesian printers (BIBO, Weedo, MethodX, etc.) are Origin at Center
@ -600,7 +600,7 @@ class PurgeLinesAndUnload(Script):
purge_volume = calculate_purge_volume(self.init_line_width, purge_len, self.mm3_per_mm)
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} X{self.machine_left + self.border_distance} Y{self.machine_front + 10} ; Move to start\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
purge_str += f"G0 F600 Z{self.purge_line_hgt} ; Move down\n"
if self.prime_blob_enable:
purge_str += adjust_for_prime_blob_gcode(self.retract_speed, self.retract_dist)
# Purge two lines
@ -611,7 +611,7 @@ class PurgeLinesAndUnload(Script):
purge_str += f"G1 F{int(self.retract_speed)} E{round(purge_volume * 2 - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z8 ; Move Up\nG4 S1 ; Wait for 1 second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} X{self.machine_left + 3 + self.border_distance} Y{self.machine_front + 20} Z0.3 ; Slide over and down\n"
purge_str += f"G0 F{self.print_speed} X{self.machine_left + 3 + self.border_distance} Y{self.machine_front + 20} Z{self.purge_line_hgt} ; Slide over and down\n"
purge_str += f"G0 X{self.machine_left + 3 + self.border_distance} Y{self.machine_front + 35} ; Wipe\n"
self.end_purge_location = Position.LEFT_FRONT
elif purge_location == Location.RIGHT:
@ -621,7 +621,7 @@ class PurgeLinesAndUnload(Script):
purge_volume = calculate_purge_volume(self.init_line_width, purge_len, self.mm3_per_mm)
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} X{self.machine_right - self.border_distance} Z2 ; Move\nG0 Y{self.machine_back - 10} Z2 ; Move to start\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
purge_str += f"G0 F600 Z{self.purge_line_hgt} ; Move down\n"
if self.prime_blob_enable:
purge_str += adjust_for_prime_blob_gcode(self.retract_speed, self.retract_dist)
# Purge two lines
@ -632,7 +632,7 @@ class PurgeLinesAndUnload(Script):
purge_str += f"G1 F{int(self.retract_speed)} E{round(purge_volume * 2 - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z8 ; Move Up\nG4 S1 ; Wait for 1 second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} X{self.machine_right - 3 - self.border_distance} Y{self.machine_back - 20} Z0.3 ; Slide over and down\n"
purge_str += f"G0 F{self.print_speed} X{self.machine_right - 3 - self.border_distance} Y{self.machine_back - 20} Z{self.purge_line_hgt} ; Slide over and down\n"
purge_str += f"G0 X{self.machine_right - 3 - self.border_distance} Y{self.machine_back - 35} ; Wipe\n"
self.end_purge_location = Position.RIGHT_REAR
elif purge_location == Location.FRONT:
@ -642,7 +642,7 @@ class PurgeLinesAndUnload(Script):
purge_volume = calculate_purge_volume(self.init_line_width, purge_len, self.mm3_per_mm)
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} X{self.machine_left + 10} Z2 ; Move\nG0 Y{self.machine_front + self.border_distance} Z2 ; Move to start\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
purge_str += f"G0 F600 Z{self.purge_line_hgt} ; Move down\n"
if self.prime_blob_enable:
purge_str += adjust_for_prime_blob_gcode(self.retract_speed, self.retract_dist)
# Purge two lines
@ -653,7 +653,7 @@ class PurgeLinesAndUnload(Script):
purge_str += f"G1 F{int(self.retract_speed)} E{round(purge_volume * 2 - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z8 ; Move Up\nG4 S1 ; Wait for 1 second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} X{self.machine_left + 20} Y{self.machine_front + 3 + self.border_distance} Z0.3 ; Slide over and down\n"
purge_str += f"G0 F{self.print_speed} X{self.machine_left + 20} Y{self.machine_front + 3 + self.border_distance} Z{self.purge_line_hgt} ; Slide over and down\n"
purge_str += f"G0 X{self.machine_left + 35} Y{self.machine_front + 3 + self.border_distance} ; Wipe\n"
self.end_purge_location = Position.LEFT_FRONT
elif purge_location == Location.REAR:
@ -664,7 +664,7 @@ class PurgeLinesAndUnload(Script):
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} Y{self.machine_back - self.border_distance} Z2; Ortho Move to back\n"
purge_str += f"G0 X{self.machine_right - 10} Z2 ; Ortho Move to start\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
purge_str += f"G0 F600 Z{self.purge_line_hgt} ; Move down\n"
if self.prime_blob_enable:
purge_str += adjust_for_prime_blob_gcode(self.retract_speed, self.retract_dist)
# Purge two lines
@ -675,7 +675,7 @@ class PurgeLinesAndUnload(Script):
purge_str += f"G1 F{int(self.retract_speed)} E{round(purge_volume * 2 - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z8 ; Move Up\nG4 S1 ; Wait for 1 second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} X{self.machine_right - 20} Y{self.machine_back - 3 - self.border_distance} Z0.3 ; Slide over and down\n"
purge_str += f"G0 F{self.print_speed} X{self.machine_right - 20} Y{self.machine_back - 3 - self.border_distance} Z{self.purge_line_hgt} ; Slide over and down\n"
purge_str += f"G0 X{self.machine_right - 35} Y{self.machine_back - 3 - self.border_distance} ; Wipe\n"
self.end_purge_location = Position.RIGHT_REAR
# Elliptic printers with Origin at Center
@ -689,7 +689,7 @@ class PurgeLinesAndUnload(Script):
if purge_location == Location.LEFT:
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} X-{round(radius_1 * .707, 2)} Y-{round(radius_1 * .707, 2)} ; Travel\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
purge_str += f"G0 F600 Z{self.purge_line_hgt} ; Move down\n"
# Purge two arcs
purge_str += f"G2 F{self.print_speed} X-{round(radius_1 * .707, 2)} Y{round(radius_1 * .707, 2)} I{round(radius_1 * .707, 2)} J{round(radius_1 * .707, 2)} E{purge_volume} ; First Arc\n"
purge_str += f"G0 X-{round((radius_1 - 3) * .707, 2)} Y{round((radius_1 - 3) * .707, 2)} ; Move Over\n"
@ -699,13 +699,13 @@ class PurgeLinesAndUnload(Script):
purge_str += f"G1 F{int(self.retract_speed)} E{round((purge_volume * 2 + 1) - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z5 ; Move Up\nG4 S1 ; Wait 1 Second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} X-{round((radius_1 - 3) * .707 - 15, 2)} Z0.3 ; Slide Over\n"
purge_str += f"G0 F{self.print_speed} X-{round((radius_1 - 3) * .707 - 15, 2)} Z{self.purge_line_hgt} ; Slide Over\n"
purge_str += f"G0 F{self.print_speed} X-{round((radius_1 - 3) * .707, 2)} ; Wipe\n"
self.end_purge_location = Position.LEFT_FRONT
elif purge_location == Location.RIGHT:
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} X{round(radius_1 * .707, 2)} Y-{round(radius_1 * .707, 2)} ; Travel\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
purge_str += f"G0 F600 Z{self.purge_line_hgt} ; Move down\n"
# Purge two arcs
purge_str += f"G3 F{self.print_speed} X{round(radius_1 * .707, 2)} Y{round(radius_1 * .707, 2)} I-{round(radius_1 * .707, 2)} J{round(radius_1 * .707, 2)} E{purge_volume} ; First Arc\n"
purge_str += f"G0 X{round((radius_1 - 3) * .707, 2)} Y{round((radius_1 - 3) * .707, 2)} ; Move Over\n"
@ -715,13 +715,13 @@ class PurgeLinesAndUnload(Script):
purge_str += f"G1 F{int(self.retract_speed)} E{round((purge_volume * 2 + 1) - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z5 ; Move Up\nG4 S1 ; Wait 1 Second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} X{round((radius_1 - 3) * .707 - 15, 2)} Z0.3 ; Slide Over\n"
purge_str += f"G0 F{self.print_speed} X{round((radius_1 - 3) * .707 - 15, 2)} Z{self.purge_line_hgt} ; Slide Over\n"
purge_str += f"G0 F{self.print_speed} X{round((radius_1 - 3) * .707, 2)} ; Wipe\n"
self.end_purge_location = Position.RIGHT_REAR
elif purge_location == Location.FRONT:
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} X-{round(radius_1 * .707, 2)} Y-{round(radius_1 * .707, 2)} ; Travel\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
purge_str += f"G0 F600 Z{self.purge_line_hgt} ; Move down\n"
# Purge two arcs
purge_str += f"G3 F{self.print_speed} X{round(radius_1 * .707, 2)} Y-{round(radius_1 * .707, 2)} I{round(radius_1 * .707, 2)} J{round(radius_1 * .707, 2)} E{purge_volume} ; First Arc\n"
purge_str += f"G0 X{round((radius_1 - 3) * .707, 2)} Y-{round((radius_1 - 3) * .707, 2)} ; Move Over\n"
@ -731,13 +731,13 @@ class PurgeLinesAndUnload(Script):
purge_str += f"G1 F{int(self.retract_speed)} E{round((purge_volume * 2 + 1) - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z5 ; Move Up\nG4 S1 ; Wait 1 Second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} Y-{round((radius_1 - 3) * .707 - 15, 2)} Z0.3 ; Slide Over\n"
purge_str += f"G0 F{self.print_speed} Y-{round((radius_1 - 3) * .707 - 15, 2)} Z{self.purge_line_hgt} ; Slide Over\n"
purge_str += f"G0 F{self.print_speed} Y-{round((radius_1 - 3) * .707, 2)} ; Wipe\n"
self.end_purge_location = Position.LEFT_FRONT
elif purge_location == Location.REAR:
# Travel to the purge start
purge_str += f"G0 F{self.speed_travel} X{round(radius_1 * .707, 2)} Y{round(radius_1 * .707, 2)} ; Travel\n"
purge_str += f"G0 F600 Z0.3 ; Move down\n"
purge_str += f"G0 F600 Z{self.purge_line_hgt} ; Move down\n"
# Purge two arcs
purge_str += f"G3 F{self.print_speed} X-{round(radius_1 * .707, 2)} Y{round(radius_1 * .707, 2)} I-{round(radius_1 * .707, 2)} J-{round(radius_1 * .707, 2)} E{purge_volume} ; First Arc\n"
purge_str += f"G0 X-{round((radius_1 - 3) * .707, 2)} Y{round((radius_1 - 3) * .707, 2)} ; Move Over\n"
@ -747,7 +747,7 @@ class PurgeLinesAndUnload(Script):
purge_str += f"G1 F{int(self.retract_speed)} E{round((purge_volume * 2 + 1) - self.retract_dist, 5)} ; Retract\n" if self.retraction_enable else ""
purge_str += "G0 F600 Z5\nG4 S1 ; Wait 1 Second\n"
# Wipe
purge_str += f"G0 F{self.print_speed} Y{round((radius_1 - 3) * .707 - 15, 2)} Z0.3 ; Slide Over\n"
purge_str += f"G0 F{self.print_speed} Y{round((radius_1 - 3) * .707 - 15, 2)} Z{self.purge_line_hgt} ; Slide Over\n"
purge_str += f"G0 F{self.print_speed} Y{round((radius_1 - 3) * .707, 2)} ; Wipe\n"
self.end_purge_location = Position.RIGHT_REAR

View file

@ -274,6 +274,7 @@ Item
value: sliderRoot.upperValue
busy: UM.SimulationView.busy
setValue: upperHandle.setValueManually // connect callback functions
layerHeight: UM.SimulationView.currentLayerHeight
}
}
@ -384,6 +385,7 @@ Item
value: sliderRoot.lowerValue
busy: UM.SimulationView.busy
setValue: lowerHandle.setValueManually // connect callback functions
layerHeight: UM.SimulationView.minimumLayerHeight
}
}
}

View file

@ -133,7 +133,8 @@ class SimulationPass(RenderPass):
nozzle_node = node
nozzle_node.setVisible(False) # Don't set to true, we render it separately!
elif getattr(node, "_outside_buildarea", False) and isinstance(node, SceneNode) and node.getMeshData() and node.isVisible() and not node.callDecoration("isNonPrintingMesh"):
elif ((getattr(node, "_outside_buildarea", False) or node.callDecoration("isAssignedToDisabledExtruder")) and
isinstance(node, SceneNode) and node.getMeshData() and node.isVisible() and not node.callDecoration("isNonPrintingMesh")):
disabled_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData())
elif isinstance(node, SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible():

View file

@ -18,6 +18,7 @@ UM.PointingRectangle
property var setValue // Function
property bool busy: false
property int startFrom: 1
property real layerHeight: 0.0 // Height in mm to display
target: Qt.point(parent.width, y + height / 2)
arrowSize: UM.Theme.getSize("button_tooltip_arrow").height
@ -84,6 +85,29 @@ UM.PointingRectangle
bottom: startFrom
top: sliderLabelRoot.maximumValue + startFrom // +startFrom because maybe we want to start in a different value rather than 0
}
Rectangle
{
id: layerHeightBackground
x: -(width + UM.Theme.getSize("narrow_margin").width)
y: (parent.height - height) / 2
width: layerHeightText.width + 2 * UM.Theme.getSize("narrow_margin").width
height: layerHeightText.height + 2 * UM.Theme.getSize("narrow_margin").height
color: UM.Theme.getColor("tool_panel_background")
radius: UM.Theme.getSize("default_radius").width
border.color: UM.Theme.getColor("lining")
border.width: UM.Theme.getSize("default_lining").width
Text
{
id: layerHeightText
anchors.centerIn: parent
text: sliderLabelRoot.layerHeight.toFixed(2) + "mm"
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering
}
}
}
BusyIndicator
{

View file

@ -101,6 +101,10 @@ class SimulationView(CuraView):
self._cumulative_line_duration_layer: Optional[int] = None
self._cumulative_line_duration: List[float] = []
# Cache for layer heights to avoid recalculating on every query
self._layer_heights_cache: dict[int, float] = {}
self._layer_heights_cache_node_id: Optional[int] = None # Track which node's data is cached
self._global_container_stack: Optional[ContainerStack] = None
self._proxy = None
@ -289,6 +293,88 @@ class SimulationView(CuraView):
return layer_data.getLayer(self.getCurrentLayer())
return None
def _calculateLayerHeightsCache(self) -> None:
"""Calculate and cache heights for all layers.
This method iterates through all layers once and stores their heights in a cache.
Handles both sliced data (microns) and loaded gcode (millimeters).
For layer 0 from gcode, uses thickness instead of height due to incorrect Z coordinates.
Only recalculates if the layer data source has changed.
"""
scene = self.getController().getScene()
from cura.Scene.GCodeListDecorator import GCodeListDecorator
for node in DepthFirstIterator(scene.getRoot()): # type: ignore
layer_data = node.callDecoration("getLayerData")
if not layer_data:
continue
# Check if we already have cached data for this layer_data object
# Use id of the layer_data itself, not the node, since node might be reused
current_layer_data_id = id(layer_data)
if self._layer_heights_cache_node_id == current_layer_data_id and self._layer_heights_cache:
# Cache is still valid, no need to recalculate
return
# Cache is invalid or empty, recalculate
self._layer_heights_cache.clear()
self._layer_heights_cache_node_id = current_layer_data_id
has_gcode_decorator = node.getDecorator(GCodeListDecorator) is not None
# Process all layers at once
for layer_id in layer_data.getLayers():
layer = layer_data.getLayer(layer_id)
if not layer:
continue
# If node has GCodeListDecorator, heights are already in millimeters (from gcode)
if has_gcode_decorator:
# Special case for layer 0: FlavorParser may get wrong Z coordinate (startup position)
# Use thickness instead, which represents the actual layer height
if layer_id == 0 and layer.thickness > 0:
self._layer_heights_cache[layer_id] = layer.thickness
else:
self._layer_heights_cache[layer_id] = layer.height
# Otherwise, heights are in microns (backend/slicing), convert to mm
else:
self._layer_heights_cache[layer_id] = layer.height / 1000.0
# We found layer data and cached it, no need to continue searching
return
# No layer data found - clear the cache
if self._layer_heights_cache_node_id is not None:
self._layer_heights_cache.clear()
self._layer_heights_cache_node_id = None
def _getLayerHeight(self, layer_number: int) -> float:
"""Helper method to get the height of a specific layer in millimeters from cache.
:param layer_number: The layer number to get the height for.
:return: The layer height in millimeters, or 0.0 if no data is available.
"""
return self._layer_heights_cache.get(layer_number, 0.0)
def getCurrentLayerHeight(self) -> float:
"""Get the height (z-coordinate) of the current layer in millimeters.
This returns the actual height from the layer data, which already takes into account:
- Initial layer height (layer_height_0)
- Adaptive layer heights
- Regular layer height
- Raft layers
Returns 0.0 if no layer data is available.
"""
return self._layer_heights_cache.get(self.getCurrentLayer(), 0.0)
def getMinimumLayerHeight(self) -> float:
"""Get the height (z-coordinate) of the minimum layer in millimeters.
Returns 0.0 if no layer data is available.
"""
return self._layer_heights_cache.get(self.getMinimumLayer(), 0.0)
def getMinimumPath(self) -> int:
return self._minimum_path_num
@ -304,6 +390,7 @@ class SimulationView(CuraView):
if node.getMeshData() is None:
return
self.setActivity(False)
self._calculateLayerHeightsCache()
self.calculateColorSchemeLimits()
self.calculateMaxLayers()
self.calculateMaxPathsOnLayer(self._current_layer_num)
@ -718,6 +805,7 @@ class SimulationView(CuraView):
Application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged)
self._calculateLayerHeightsCache()
self.calculateColorSchemeLimits()
self.calculateMaxLayers()
self.calculateMaxPathsOnLayer(self._current_layer_num)

View file

@ -53,17 +53,40 @@ Cura.ExpandableComponent
UM.Label
{
id: schemeTypeLabel
text: layerTypeCombobox.currentText
anchors
{
left: colorSchemeLabel.right
leftMargin: UM.Theme.getSize("default_margin").width
right: parent.right
}
anchors.left: colorSchemeLabel.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
height: parent.height
elide: Text.ElideRight
font: UM.Theme.getFont("medium")
}
UM.ColorImage
{
id: warningIcon
anchors
{
left: schemeTypeLabel.right
leftMargin: UM.Theme.getSize("narrow_margin").width
verticalCenter: parent.verticalCenter
}
width: UM.Theme.getSize("section_icon").width
height: UM.Theme.getSize("section_icon").height
source: UM.Theme.getIcon("Warning")
color: UM.Theme.getColor("warning")
visible: {
// Check if any enabled extruder is unchecked
var extrudersModel = CuraApplication.getExtrudersModel();
for (var i = 0; i < extrudersModel.count; i++) {
var extruder = extrudersModel.getItem(i);
if (extruder.enabled && viewSettings.extruder_opacities[i] <= 0.5 && viewSettings.extruder_opacities[i] !== undefined && viewSettings.extruder_opacities[i] !== "") {
return true;
}
}
return false;
}
}
}
contentItem: Column
@ -212,11 +235,27 @@ Cura.ExpandableComponent
{
verticalCenter: parent.verticalCenter
left: extrudersModelCheckBox.left
right: extrudersModelCheckBox.right
right: extruderWarningIcon.visible ? extruderWarningIcon.left : swatch.left
leftMargin: UM.Theme.getSize("checkbox").width + Math.round(UM.Theme.getSize("default_margin").width / 2)
rightMargin: UM.Theme.getSize("default_margin").width * 2
rightMargin: UM.Theme.getSize("narrow_margin").width
}
}
UM.ColorImage
{
id: extruderWarningIcon
anchors
{
verticalCenter: parent.verticalCenter
right: swatch.left
rightMargin: UM.Theme.getSize("narrow_margin").width
}
width: UM.Theme.getSize("section_icon").width
height: UM.Theme.getSize("section_icon").height
source: UM.Theme.getIcon("Warning")
color: UM.Theme.getColor("warning")
visible: model.enabled && !extrudersModelCheckBox.checked
}
}
}

View file

@ -46,6 +46,14 @@ class SimulationViewProxy(QObject):
def minimumLayer(self):
return self._simulation_view.getMinimumLayer()
@pyqtProperty(float, notify=currentLayerChanged)
def currentLayerHeight(self):
return self._simulation_view.getCurrentLayerHeight()
@pyqtProperty(float, notify=currentLayerChanged)
def minimumLayerHeight(self):
return self._simulation_view.getMinimumLayerHeight()
@pyqtProperty(int, notify=maxPathsChanged)
def numPaths(self):
return self._simulation_view.getMaxPaths()

View file

@ -1,7 +1,7 @@
{
"name": "Simulation View",
"author": "Ultimaker B.V.",
"version": "1.0.1",
"version": "1.1.0",
"description": "Provides the preview of sliced layerdata.",
"api": 8,
"i18n-catalog": "cura"

View file

@ -270,7 +270,7 @@ class SolidView(View):
renderer.queueNode(node, shader = self._non_printing_shader, uniforms = uniforms, transparent = True)
else:
renderer.queueNode(node, shader = self._non_printing_shader, transparent = True)
elif getattr(node, "_outside_buildarea", False):
elif getattr(node, "_outside_buildarea", False) or node.callDecoration("isAssignedToDisabledExtruder"):
disabled_batch.addItem(node.getWorldTransformation(copy = False), node.getMeshData(), normal_transformation = node.getCachedNormalMatrix())
elif per_mesh_stack and node.callDecoration("isSupportMesh"):
# Render support meshes with a vertical stripe that is darker

View file

@ -0,0 +1,34 @@
# Copyright (c) 2025 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Message import Message
class AuthorizationRequiredMessage:
_inner_message_instance = None
class InnerMessage(Message):
def __init__(self, printer_name: str, err_message: str) -> None:
super().__init__(
text = printer_name,
title = err_message,
message_type = Message.MessageType.WARNING,
lifetime = 0
)
@classmethod
def _getInstance(cls) -> Message:
if cls._inner_message_instance is None:
cls._inner_message_instance = cls.InnerMessage("", "")
return cls._inner_message_instance
@classmethod
def show(cls, printer_name: str, err_message: str) -> None:
msg = cls._getInstance()
msg.setText(printer_name)
msg.setTitle(err_message)
msg.show()
@classmethod
def hide(cls) -> None:
cls._getInstance().hide()

View file

@ -16,6 +16,7 @@ from UM.Logger import Logger
from cura.CuraApplication import CuraApplication
from ..Messages.AuthorizationRequiredMessage import AuthorizationRequiredMessage
from ..Models.BaseModel import BaseModel
from ..Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus
from ..Models.Http.ClusterPrinterStatus import ClusterPrinterStatus
@ -56,7 +57,7 @@ class ClusterApiClient:
# In order to avoid garbage collection we keep the callbacks in this list.
_anti_gc_callbacks = [] # type: List[Callable[[], None]]
def __init__(self, address: str, on_error: Callable) -> None:
def __init__(self, address: str, on_error: Callable, on_auth_required: Callable) -> None:
"""Initializes a new cluster API client.
:param address: The network address of the cluster to call.
@ -68,6 +69,7 @@ class ClusterApiClient:
self._on_error = on_error
self._auth_tries = 0
self._on_auth_required = on_auth_required
prefs = CuraApplication.getInstance().getPreferences()
prefs.addPreference("cluster_api/auth_ids", "{}")
@ -302,6 +304,10 @@ class ClusterApiClient:
if reply.error() != QNetworkReply.NetworkError.NoError:
if reply.error() == QNetworkReply.NetworkError.AuthenticationRequiredError:
self._auth_id = None
self._auth_key = None
self._on_auth_required(reply.errorString())
nonce_match = re.search(r'nonce="([^"]+)', str(reply.rawHeader(b"WWW-Authenticate")))
if nonce_match:
self._nonce = nonce_match.group(1)
@ -309,9 +315,13 @@ class ClusterApiClient:
self._setLocalValueToPrefDict("cluster_api/nonce_counts", self._nonce_count)
self._setLocalValueToPrefDict("cluster_api/nonces", self._nonce)
CuraApplication.getInstance().savePreferences()
self._on_error(reply.errorString())
else:
self._on_error(reply.errorString())
return
if self._auth_id and self._auth_key and self._nonce_count > 1:
AuthorizationRequiredMessage.hide()
# If no parse model is given, simply return the raw data in the callback.
if not model:
on_finished(reply.readAll())

View file

@ -1,6 +1,7 @@
# Copyright (c) 2020 Ultimaker B.V.
# Copyright (c) 2025 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
import os
import platform
from typing import Optional, Dict, List, Callable, Any
from time import time
@ -21,6 +22,7 @@ from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
from .ClusterApiClient import ClusterApiClient
from .SendMaterialJob import SendMaterialJob
from ..ExportFileJob import ExportFileJob
from ..Messages.AuthorizationRequiredMessage import AuthorizationRequiredMessage
from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice
from ..Messages.PrintJobUploadBlockedMessage import PrintJobUploadBlockedMessage
from ..Messages.PrintJobUploadErrorMessage import PrintJobUploadErrorMessage
@ -194,7 +196,7 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
return
self._progress.show()
parts = [
self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"),
self._createFormPart("name=owner", bytes(f"user@{platform.node()}", "utf-8"), "text/plain"),
self._createFormPart("name=\"file\"; filename=\"%s\"" % self._active_exported_job.getFileName(),
self._active_exported_job.getOutput())
]
@ -239,9 +241,19 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
if print_job.getPreviewImage() is None:
self.getApiClient().getPrintJobPreviewImage(print_job.key, print_job.updatePreviewImageData)
def _onAuthRequired(self, error_msg: str) -> None:
active_name = CuraApplication.getInstance().getOutputDeviceManager().getActiveDevice().getName()
if self._name == active_name:
Logger.info(f"Authorization required for {self._name}: {error_msg}")
AuthorizationRequiredMessage.show(self._name, error_msg)
def getApiClient(self) -> ClusterApiClient:
"""Get the API client instance."""
if not self._cluster_api:
self._cluster_api = ClusterApiClient(self.address, on_error = lambda error: Logger.log("e", str(error)))
self._cluster_api = ClusterApiClient(
self._address,
on_error = lambda error: Logger.log("e", str(error)),
on_auth_required = self._onAuthRequired
)
return self._cluster_api

View file

@ -15,6 +15,7 @@ from cura.Settings.GlobalStack import GlobalStack
from .ZeroConfClient import ZeroConfClient
from .ClusterApiClient import ClusterApiClient
from .LocalClusterOutputDevice import LocalClusterOutputDevice
from ..Messages.AuthorizationRequiredMessage import AuthorizationRequiredMessage
from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice
from ..Messages.CloudFlowMessage import CloudFlowMessage
from ..Messages.LegacyDeviceNoLongerSupportedMessage import LegacyDeviceNoLongerSupportedMessage
@ -44,6 +45,7 @@ class LocalClusterOutputDeviceManager:
# Persistent dict containing the networked clusters.
self._discovered_devices = {} # type: Dict[str, LocalClusterOutputDevice]
self._output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
self._output_device_manager.activeDeviceChanged.connect(self._onActiveDeviceChanged)
# Hook up ZeroConf client.
self._zero_conf_client = ZeroConfClient()
@ -69,10 +71,17 @@ class LocalClusterOutputDeviceManager:
self.stop()
self.start()
def _onActiveDeviceChanged(self):
AuthorizationRequiredMessage.hide()
def _onAuthRequired(self, error_msg: str) -> None:
Logger.info(f"Authorization required: {error_msg}")
AuthorizationRequiredMessage.show("", error_msg)
def addManualDevice(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None:
"""Add a networked printer manually by address."""
api_client = ClusterApiClient(address, lambda error: Logger.log("e", str(error)))
api_client = ClusterApiClient(address, lambda error: Logger.log("e", str(error)), self._onAuthRequired)
api_client.getSystem(lambda status: self._onCheckManualDeviceResponse(address, status, callback))
def removeManualDevice(self, device_id: str, address: Optional[str] = None) -> None:

View file

@ -39,7 +39,7 @@
"machine_acceleration": { "value": 3000 },
"machine_center_is_zero": { "default_value": false },
"machine_depth": { "default_value": 210 },
"machine_end_gcode": { "default_value": "M104 S0 ; Extruder off \nM140 S0 ; Heatbed off \nM107 ; Fan off \nG91 ; relative positioning \nG1 E-5 F300 ; retract a little \nG1 Z+10 E-5 ; X-20 Y-20 F{travel_xy_speed} ; lift print head \nG28 X0 Y0 ; homing \nG1 Y180 F2000 ; reset feedrate \nM84 ; disable stepper motors \nG90 ; absolute positioning \nM300 S440 P200 ; Make Print Completed Tones \nM300 S660 P250 ; beep \nM300 S880 P300 ; beep" },
"machine_end_gcode": { "default_value": "M104 S0 ; Extruder off \nM140 S0 ; Heatbed off \nM107 ; Fan off \nG91 ; relative positioning \nG1 E-5 F300 ; retract a little \nG1 Z+10 E-5 ; X-20 Y-20 F{speed_travel} ; lift print head \nG28 X0 Y0 ; homing \nG1 Y180 F2000 ; reset feedrate \nM84 ; disable stepper motors \nG90 ; absolute positioning \nM300 S440 P200 ; Make Print Completed Tones \nM300 S660 P250 ; beep \nM300 S880 P300 ; beep" },
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
"machine_heated_bed": { "default_value": true },
"machine_height": { "default_value": 205 },

View file

@ -486,6 +486,19 @@
"settable_per_extruder": false,
"settable_per_meshgroup": false
},
"machine_time_estimation_factor":
{
"label": "Print Time Estimation Factor",
"description": "Multiplier for adjusting print time estimates. 100% shows original estimate. Higher values indicate longer actual print times.",
"default_value": 100,
"minimum_value": 1,
"maximum_value": 1000,
"type": "float",
"unit": "%",
"settable_per_mesh": false,
"settable_per_extruder": false,
"settable_per_meshgroup": false
},
"machine_nozzle_id":
{
"label": "Nozzle ID",
@ -1543,15 +1556,15 @@
"limit_to_extruder": "roofing_extruder_nr",
"settable_per_mesh": true
},
"roofing_extension":
"roofing_expansion":
{
"label": "Top Surface Extension",
"description": "Determines how much the top surfaces are extended beneath overlapping surfaces. By adjusting this value, you can ensure that the outer edges of the top surfaces are concealed by the layers above, resulting in a better visual quality, particularly for models with curved surfaces.",
"label": "Top Surface Expansion",
"description": "Determines how much the top surfaces are expanded beneath overlapping surfaces. By adjusting this value, you can ensure that the outer edges of the top surfaces are concealed by the layers above, resulting in a better visual quality, particularly for models with curved surfaces.",
"type": "float",
"default_value": "0",
"value": "0",
"minimum_value": "0",
"maximum_value_warning": "roofing_line_width * 10",
"maximum_value_warning": "roofing_line_width * 3",
"enabled": "roofing_layer_count > 0 and top_layers > 0",
"limit_to_extruder": "roofing_extruder_nr",
"settable_per_mesh": true
@ -2554,6 +2567,53 @@
"value": "lightning_infill_support_angle"
}
}
},
"infill_move_inwards_length":
{
"label": "Infill Start/End Move Inwards Length",
"description": "When starting or ending infill print, add an inwards extrusion move so that the tips of the infill won't impact the outer wall. This can be useful when the infill is printed at very high speed.",
"unit": "mm",
"type": "float",
"minimum_value": "0",
"maximum_value_warning": "line_width * 10",
"default_value": 0,
"limit_to_extruder": "infill_extruder_nr",
"enabled": "infill_sparse_density > 0",
"settable_per_mesh": true,
"settable_per_extruder": true,
"children":
{
"infill_start_move_inwards_length":
{
"label": "Infill Start Move Inwards Length",
"description": "When starting infill print, add an inwards extrusion move so that the tips of the infill won't impact the outer wall. This can be useful when the infill is printed at very high speed.",
"unit": "mm",
"type": "float",
"minimum_value": "0",
"maximum_value_warning": "line_width * 10",
"default_value": 0,
"value": "infill_move_inwards_length",
"limit_to_extruder": "infill_extruder_nr",
"enabled": "infill_sparse_density > 0",
"settable_per_mesh": true,
"settable_per_extruder": true
},
"infill_end_move_inwards_length":
{
"label": "Infill End Move Inwards Length",
"description": "When starting or ending infill print, add an inwards extrusion move so that the tips of the infill won't impact the outer wall. This can be useful when the infill is printed at very high speed.",
"unit": "mm",
"type": "float",
"minimum_value": "0",
"maximum_value_warning": "line_width * 10",
"default_value": 0,
"value": "infill_move_inwards_length",
"limit_to_extruder": "infill_extruder_nr",
"enabled": "infill_sparse_density > 0",
"settable_per_mesh": true,
"settable_per_extruder": true
}
}
}
}
},
@ -7939,13 +7999,13 @@
"minimum_value": "0.05",
"maximum_value": "5",
"maximum_value_warning": "line_width * 2",
"settable_per_mesh": false,
"settable_per_extruder": false
"settable_per_mesh": true,
"settable_per_extruder": true
},
"multi_material_paint_deepness":
"multi_material_paint_depth":
{
"label": "Multi-material Deepness",
"description": "The deepness of the painted details inside the model. A higher deepness will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated deepness may vary.",
"label": "Multi-material Depth",
"description": "The depth of the painted details inside the model. A higher depth will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated depth may vary.",
"unit": "mm",
"type": "float",
"enabled": "extruders_enabled_count > 1",
@ -7953,8 +8013,8 @@
"value": "line_width * 10",
"minimum_value": "line_width",
"minimum_value_warning": "line_width * 2",
"settable_per_mesh": false,
"settable_per_extruder": false
"settable_per_mesh": true,
"settable_per_extruder": true
}
}
},
@ -8964,6 +9024,15 @@
"enabled": "bridge_settings_enabled",
"settable_per_mesh": true
},
"bridge_interlace_lines":
{
"label": "Interlace Bridge Lines",
"description": "When enabled, bridging lines will be printed interlaced, i.e. in 2 monotonic passes so that adjacent lines won't be printed just after each other. The lines of the first pass then have more time to cool down and are stronger when the second pass is added.",
"default_value": false,
"type": "bool",
"enabled": "bridge_settings_enabled",
"settable_per_mesh": true
},
"bridge_enable_more_layers":
{
"label": "Bridge Has Multiple Layers",

View file

@ -1,6 +1,6 @@
{
"version": 2,
"name": "Toybox Alpha One/Two",
"name": "Toybox Alpha One/Two/Three",
"inherits": "fdmprinter",
"metadata":
{
@ -17,7 +17,7 @@
"machine_depth": { "default_value": 80 },
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
"machine_height": { "default_value": 90 },
"machine_name": { "default_value": "Toybox Alpha One/Two" },
"machine_name": { "default_value": "Toybox Alpha One/Two/Three" },
"machine_start_gcode": { "default_value": "G90\nM82" },
"machine_width": { "default_value": 70 },
"material_diameter": { "default_value": 1.75 }

View file

@ -240,11 +240,12 @@
"gradual_flow_enabled": { "value": true },
"hole_xy_offset": { "value": 0.075 },
"infill_material_flow": { "value": "material_flow if infill_sparse_density < 95 else 95" },
"infill_move_inwards_length": { "value": "3*machine_nozzle_size" },
"infill_overlap": { "value": 10 },
"infill_pattern": { "value": "'zigzag' if infill_sparse_density > 50 else 'grid'" },
"infill_sparse_density": { "value": 15 },
"infill_wall_line_count": { "value": "1 if infill_sparse_density > 80 else 0" },
"initial_bottom_layers": { "value": "2 if extruderValueFromContainer(extruder_nr, 'bottom_layers', 2) == bottom_layers else bottom_layers" },
"initial_bottom_layers": { "value": "2 if extruderValueFromContainer(top_bottom_extruder_nr, 'bottom_layers', 2) == bottom_layers else bottom_layers" },
"jerk_flooring":
{
"maximum_value_warning": "machine_max_jerk_xy / 2",
@ -432,6 +433,11 @@
"max_flow_acceleration": { "value": 1.5 },
"max_skin_angle_for_expansion": { "value": 45 },
"meshfix_maximum_resolution": { "value": 0.4 },
"min_bead_width":
{
"maximum_value_warning": "machine_nozzle_size * 1.25",
"value": "machine_nozzle_size * 1.15"
},
"min_infill_area": { "default_value": 10 },
"optimize_wall_printing_order": { "value": false },
"prime_during_travel_ratio": { "enabled": false },
@ -446,7 +452,7 @@
"retraction_hop_after_extruder_switch_height": { "value": 2 },
"retraction_hop_enabled": { "value": true },
"retraction_min_travel": { "value": "2.5 if support_enable and support_structure=='tree' else line_width * 2.5" },
"roofing_extension": { "value": 1.2 },
"roofing_expansion": { "value": 1.2 },
"roofing_pattern": { "value": "'lines'" },
"seam_overhang_angle": { "value": 35 },
"skin_edge_support_thickness": { "value": 0.8 },
@ -607,6 +613,7 @@
"support_xy_distance": { "value": 1.2 },
"support_xy_distance_overhang": { "value": "1.5*machine_nozzle_size" },
"support_z_distance": { "value": "2*layer_height" },
"switch_extruder_retraction_amount": { "value": 16 },
"top_bottom_thickness": { "value": "wall_thickness" },
"travel_avoid_other_parts": { "value": true },
"travel_avoid_supports": { "value": true },

View file

@ -91,7 +91,7 @@
"cool_fan_speed_0": { "value": 0 },
"cool_min_layer_time": { "value": 8 },
"extruder_prime_pos_abs": { "default_value": true },
"fill_outline_gaps": { "value": false },
"fill_outline_gaps": { "value": true },
"gantry_height": { "value": "60" },
"infill_angles": { "value": "[45,45,45,45,45,135,135,135,135,135]" },
"infill_before_walls": { "value": false },
@ -106,7 +106,7 @@
"enabled": false,
"value": false
},
"layer_height_0": { "value": "layer_height * 1.25" },
"layer_height_0": { "value": "layer_height * 2" },
"layer_start_x": { "value": "sum(extruderValues('machine_extruder_start_pos_x')) / len(extruderValues('machine_extruder_start_pos_x'))" },
"layer_start_y": { "value": "sum(extruderValues('machine_extruder_start_pos_y')) / len(extruderValues('machine_extruder_start_pos_y'))" },
"machine_center_is_zero": { "default_value": true },
@ -146,7 +146,7 @@
{
"minimum_value": "line_width * 0.5",
"minimum_value_warning": "line_width * 0.75",
"value": "line_width"
"value": "line_width * 1.15"
},
"min_wall_line_width":
{

View file

@ -178,7 +178,7 @@
"maximum_value": 260,
"maximum_value_warning": 240
},
"min_bead_width": { "value": 0.3 },
"min_bead_width": { "value": "line_width * 1.15" },
"multiple_mesh_overlap": { "value": "0" },
"print_sequence": { "enabled": false },
"raft_airgap": { "value": 0.35 },

View file

@ -3151,12 +3151,10 @@ msgctxt "@info:status"
msgid ""
"Please review settings and check if your models:\n"
"- Fit within the build volume\n"
"- Are assigned to an enabled extruder\n"
"- Are not all set as modifier meshes"
msgstr ""
"Zkontrolujte nastavení a zda vaše modely:\n"
"- Vejdou se na pracovní prostor\n"
"- Jsou přiřazeny k povolenému extruderu\n"
"- Nejsou nastavené jako modifikační sítě"
msgctxt "@label"

View file

@ -2565,8 +2565,8 @@ msgctxt "skin_monotonic label"
msgid "Monotonic Top/Bottom Order"
msgstr "Monotónní pořadí horních / dolních povrchů"
msgctxt "multi_material_paint_deepness label"
msgid "Multi-material Deepness"
msgctxt "multi_material_paint_depth label"
msgid "Multi-material Depth"
msgstr ""
msgctxt "multi_material_paint_resolution label"
@ -4329,8 +4329,8 @@ msgctxt "material_brand description"
msgid "The brand of material used."
msgstr ""
msgctxt "multi_material_paint_deepness description"
msgid "The deepness of the painted details inside the model. A higher deepness will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated deepness may vary."
msgctxt "multi_material_paint_depth description"
msgid "The depth of the painted details inside the model. A higher depth will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated depth may vary."
msgstr ""
msgctxt "machine_acceleration description"

View file

@ -3002,7 +3002,6 @@ msgctxt "@info:status"
msgid ""
"Please review settings and check if your models:\n"
"- Fit within the build volume\n"
"- Are assigned to an enabled extruder\n"
"- Are not all set as modifier meshes"
msgstr ""

View file

@ -3035,10 +3035,9 @@ msgid "Please remove the print"
msgstr "Bitte den Ausdruck entfernen"
msgctxt "@info:status"
msgid "Please review settings and check if your models:\n- Fit within the build volume\n- Are assigned to an enabled extruder\n- Are not all set as modifier meshes"
msgid "Please review settings and check if your models:\n- Fit within the build volume\n- Are not all set as modifier meshes"
msgstr "Bitte überprüfen Sie die Einstellungen und prüfen Sie, ob Ihre Modelle:"
"- Mit der Druckraumgröße kompatibel sind"
"- Einem aktiven Extruder zugewiesen sind"
"- Nicht alle als Modifier Meshes eingerichtet sind"
msgctxt "@label"

View file

@ -6289,8 +6289,8 @@ msgctxt "material_max_flowrate description"
msgid "Maximum flow rate that the printer can extrude for the material"
msgstr "Die maximale Flow Rate, mit der der Drucker mit dem verwendeten Material extrudieren kann"
msgctxt "multi_material_paint_deepness label"
msgid "Multi-material Deepness"
msgctxt "multi_material_paint_depth label"
msgid "Multi-material Depth"
msgstr "Multi-Material-Tiefe"
msgctxt "multi_material_paint_resolution label"
@ -6309,8 +6309,8 @@ msgctxt "machine_scan_first_layer label"
msgid "Scan the first layer"
msgstr "Erste Schicht scannen"
msgctxt "multi_material_paint_deepness description"
msgid "The deepness of the painted details inside the model. A higher deepness will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated deepness may vary."
msgctxt "multi_material_paint_depth description"
msgid "The depth of the painted details inside the model. A higher depth will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated depth may vary."
msgstr "Die Tiefe der bemalten Details innerhalb des Modells. Eine höhere Tiefe ermöglicht eine bessere Verzahnung, steigert jedoch die Slicing-Zeit und den Speicherbedarf. Wählen Sie einen sehr hohen Wert, um eine möglichst große Tiefe zu erreichen. Die tatsächlich berechnete Tiefe kann variieren."
msgctxt "build_volume_fan_speed description"

View file

@ -3036,10 +3036,9 @@ msgid "Please remove the print"
msgstr "Retire la impresión"
msgctxt "@info:status"
msgid "Please review settings and check if your models:\n- Fit within the build volume\n- Are assigned to an enabled extruder\n- Are not all set as modifier meshes"
msgid "Please review settings and check if your models:\n- Fit within the build volume\n- Are not all set as modifier meshes"
msgstr "Revise la configuración y compruebe si sus modelos:"
"- Se integran en el volumen de impresión"
"- Están asignados a un extrusor activado"
"- No están todos definidos como mallas modificadoras"
msgctxt "@label"

View file

@ -6289,8 +6289,8 @@ msgctxt "material_max_flowrate description"
msgid "Maximum flow rate that the printer can extrude for the material"
msgstr "Tasa de flujo máximo que la impresora puede extruir para el material"
msgctxt "multi_material_paint_deepness label"
msgid "Multi-material Deepness"
msgctxt "multi_material_paint_depth label"
msgid "Multi-material Depth"
msgstr "Profundidad multimaterial"
msgctxt "multi_material_paint_resolution label"
@ -6309,8 +6309,8 @@ msgctxt "machine_scan_first_layer label"
msgid "Scan the first layer"
msgstr "Escanear la primera capa"
msgctxt "multi_material_paint_deepness description"
msgid "The deepness of the painted details inside the model. A higher deepness will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated deepness may vary."
msgctxt "multi_material_paint_depth description"
msgid "The depth of the painted details inside the model. A higher depth will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated depth may vary."
msgstr "La profundidad de los detalles pintados dentro del modelo. Una profundidad más alta proporcionará un mejor enclavamiento, pero aumentará el tiempo de trozeado y la memoria. Establezca un valor muy alto para que vaya lo más profundo posible. La profundidad calculada exactamente puede variar."
msgctxt "build_volume_fan_speed description"

View file

@ -5104,12 +5104,12 @@ msgctxt "multi_material_paint_resolution description"
msgid "The precision of the details when generating multi-material shapes based on painting data. A lower precision will provide more details, but increase the slicing time and memory."
msgstr ""
msgctxt "multi_material_paint_deepness label"
msgid "Multi-material Deepness"
msgctxt "multi_material_paint_depth label"
msgid "Multi-material Depth"
msgstr ""
msgctxt "multi_material_paint_deepness description"
msgid "The deepness of the painted details inside the model. A higher deepness will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated deepness may vary."
msgctxt "multi_material_paint_depth description"
msgid "The depth of the painted details inside the model. A higher depth will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated depth may vary."
msgstr ""
msgctxt "meshfix label"

View file

@ -3121,7 +3121,6 @@ msgctxt "@info:status"
msgid ""
"Please review settings and check if your models:\n"
"- Fit within the build volume\n"
"- Are assigned to an enabled extruder\n"
"- Are not all set as modifier meshes"
msgstr ""

View file

@ -2560,8 +2560,8 @@ msgctxt "skin_monotonic label"
msgid "Monotonic Top/Bottom Order"
msgstr ""
msgctxt "multi_material_paint_deepness label"
msgid "Multi-material Deepness"
msgctxt "multi_material_paint_depth label"
msgid "Multi-material Depth"
msgstr ""
msgctxt "multi_material_paint_resolution label"
@ -4324,8 +4324,8 @@ msgctxt "material_brand description"
msgid "The brand of material used."
msgstr ""
msgctxt "multi_material_paint_deepness description"
msgid "The deepness of the painted details inside the model. A higher deepness will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated deepness may vary."
msgctxt "multi_material_paint_depth description"
msgid "The depth of the painted details inside the model. A higher depth will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated depth may vary."
msgstr ""
msgctxt "machine_acceleration description"

View file

@ -3034,10 +3034,9 @@ msgid "Please remove the print"
msgstr "Supprimez l'imprimante"
msgctxt "@info:status"
msgid "Please review settings and check if your models:\n- Fit within the build volume\n- Are assigned to an enabled extruder\n- Are not all set as modifier meshes"
msgid "Please review settings and check if your models:\n- Fit within the build volume\n- Are not all set as modifier meshes"
msgstr "Veuillez vérifier les paramètres et si vos modèles:"
"- S'intègrent dans le volume de fabrication"
"- Sont affectés à un extrudeur activé"
"- N sont pas tous définis comme des mailles de modificateur"
msgctxt "@label"

View file

@ -6289,8 +6289,8 @@ msgctxt "material_max_flowrate description"
msgid "Maximum flow rate that the printer can extrude for the material"
msgstr "Débit maximum que l'imprimante peut extruder pour le matériau."
msgctxt "multi_material_paint_deepness label"
msgid "Multi-material Deepness"
msgctxt "multi_material_paint_depth label"
msgid "Multi-material Depth"
msgstr "Profondeur multi-matériaux"
msgctxt "multi_material_paint_resolution label"
@ -6309,8 +6309,8 @@ msgctxt "machine_scan_first_layer label"
msgid "Scan the first layer"
msgstr "Numériser la première couche"
msgctxt "multi_material_paint_deepness description"
msgid "The deepness of the painted details inside the model. A higher deepness will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated deepness may vary."
msgctxt "multi_material_paint_depth description"
msgid "The depth of the painted details inside the model. A higher depth will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated depth may vary."
msgstr "La profondeur des détails peints à l'intérieur du modèle. Une plus grande profondeur permettra un meilleur emboîtement, mais augmentera le temps de découpage et la mémoire. Définissez une valeur très élevée pour aller aussi loin que possible. La profondeur réellement calculée peut varier."
msgctxt "build_volume_fan_speed description"

View file

@ -3135,7 +3135,6 @@ msgctxt "@info:status"
msgid ""
"Please review settings and check if your models:\n"
"- Fit within the build volume\n"
"- Are assigned to an enabled extruder\n"
"- Are not all set as modifier meshes"
msgstr ""

View file

@ -2567,8 +2567,8 @@ msgctxt "skin_monotonic label"
msgid "Monotonic Top/Bottom Order"
msgstr ""
msgctxt "multi_material_paint_deepness label"
msgid "Multi-material Deepness"
msgctxt "multi_material_paint_depth label"
msgid "Multi-material Depth"
msgstr ""
msgctxt "multi_material_paint_resolution label"
@ -4331,8 +4331,8 @@ msgctxt "material_brand description"
msgid "The brand of material used."
msgstr ""
msgctxt "multi_material_paint_deepness description"
msgid "The deepness of the painted details inside the model. A higher deepness will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated deepness may vary."
msgctxt "multi_material_paint_depth description"
msgid "The depth of the painted details inside the model. A higher depth will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated depth may vary."
msgstr ""
msgctxt "machine_acceleration description"

View file

@ -3036,10 +3036,9 @@ msgid "Please remove the print"
msgstr "Rimuovere la stampa"
msgctxt "@info:status"
msgid "Please review settings and check if your models:\n- Fit within the build volume\n- Are assigned to an enabled extruder\n- Are not all set as modifier meshes"
msgid "Please review settings and check if your models:\n- Fit within the build volume\n- Are not all set as modifier meshes"
msgstr "Verificare le impostazioni e controllare se i modelli:"
"- Rientrano nel volume di stampa"
"- Sono assegnati a un estrusore abilitato"
"- Non sono tutti impostati come maglie modificatore"
msgctxt "@label"

View file

@ -6290,8 +6290,8 @@ msgctxt "material_max_flowrate description"
msgid "Maximum flow rate that the printer can extrude for the material"
msgstr "Il flusso massimo estrudibile per il materiale"
msgctxt "multi_material_paint_deepness label"
msgid "Multi-material Deepness"
msgctxt "multi_material_paint_depth label"
msgid "Multi-material Depth"
msgstr "Profondità multimateriale"
msgctxt "multi_material_paint_resolution label"
@ -6310,8 +6310,8 @@ msgctxt "machine_scan_first_layer label"
msgid "Scan the first layer"
msgstr "Scansiona il primo strato"
msgctxt "multi_material_paint_deepness description"
msgid "The deepness of the painted details inside the model. A higher deepness will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated deepness may vary."
msgctxt "multi_material_paint_depth description"
msgid "The depth of the painted details inside the model. A higher depth will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated depth may vary."
msgstr "La profondità del dettaglio dipinto nel modello. Una profondità maggiore fornirà un incastro migliore ma aumenterà il tempo di slicing e la memoria utilizzata. Imposta un valore molto alto per andare più possibile in profondità. La profondità reale calcolata può variare."
msgctxt "build_volume_fan_speed description"

View file

@ -3139,12 +3139,10 @@ msgctxt "@info:status"
msgid ""
"Please review settings and check if your models:\n"
"- Fit within the build volume\n"
"- Are assigned to an enabled extruder\n"
"- Are not all set as modifier meshes"
msgstr ""
"設定を見直し、モデルが次の状態かどうかを確認してください。\n"
"- 造形サイズに合っている\n"
"- 有効なエクストルーダーに割り当てられている\n"
"- すべてのセットが修飾子メッシュとして設定されていない"
msgctxt "@label"

View file

@ -2560,8 +2560,8 @@ msgctxt "skin_monotonic label"
msgid "Monotonic Top/Bottom Order"
msgstr "上面/底面の方向一貫性"
msgctxt "multi_material_paint_deepness label"
msgid "Multi-material Deepness"
msgctxt "multi_material_paint_depth label"
msgid "Multi-material Depth"
msgstr "複数材料の深さ"
msgctxt "multi_material_paint_resolution label"
@ -4324,8 +4324,8 @@ msgctxt "material_brand description"
msgid "The brand of material used."
msgstr "使用されている材料のブランドです。"
msgctxt "multi_material_paint_deepness description"
msgid "The deepness of the painted details inside the model. A higher deepness will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated deepness may vary."
msgctxt "multi_material_paint_depth description"
msgid "The depth of the painted details inside the model. A higher depth will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated depth may vary."
msgstr "ペイントした部分に対するモデル内部の深さです。より深くすることでより強く結合しますが、メモリー使用量とスライスにかかる時間が増加します。出来るだけ深くなるよう高い値を設定してください。実際に計算される深さは変わる場合があります。"
msgctxt "machine_acceleration description"

View file

@ -3028,10 +3028,9 @@ msgid "Please remove the print"
msgstr "프린트물을 제거하십시오"
msgctxt "@info:status"
msgid "Please review settings and check if your models:\n- Fit within the build volume\n- Are assigned to an enabled extruder\n- Are not all set as modifier meshes"
msgid "Please review settings and check if your models:\n- Fit within the build volume\n- Are not all set as modifier meshes"
msgstr "설정을 검토하고 모델이 다음 사항에 해당하는지 확인하십시오."
"- 출력 사이즈 내에 맞춤화됨"
"- 활성화된 익스트루더로 할당됨"
"- 수정자 메쉬로 전체 설정되지 않음"
msgctxt "@label"

View file

@ -6289,8 +6289,8 @@ msgctxt "material_max_flowrate description"
msgid "Maximum flow rate that the printer can extrude for the material"
msgstr "프린터가 재료를 압출할 때 허용할 최대 유량"
msgctxt "multi_material_paint_deepness label"
msgid "Multi-material Deepness"
msgctxt "multi_material_paint_depth label"
msgid "Multi-material Depth"
msgstr "다중 재료 깊이"
msgctxt "multi_material_paint_resolution label"
@ -6309,8 +6309,8 @@ msgctxt "machine_scan_first_layer label"
msgid "Scan the first layer"
msgstr "첫 레이어 스캔"
msgctxt "multi_material_paint_deepness description"
msgid "The deepness of the painted details inside the model. A higher deepness will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated deepness may vary."
msgctxt "multi_material_paint_depth description"
msgid "The depth of the painted details inside the model. A higher depth will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated depth may vary."
msgstr "모델 내부에 페인트되는 디테일의 깊이. 깊이 값이 높을수록 맞물림 정도가 향상되지만, 슬라이싱 시간과 메모리는 늘어납니다. 깊이를 최대한으로 표현하려면 아주 높은 값을 설정하세요. 실제 계산되는 깊이는 다를 수 있습니다."
msgctxt "build_volume_fan_speed description"

View file

@ -3034,10 +3034,9 @@ msgid "Please remove the print"
msgstr "Verwijder de print"
msgctxt "@info:status"
msgid "Please review settings and check if your models:\n- Fit within the build volume\n- Are assigned to an enabled extruder\n- Are not all set as modifier meshes"
msgid "Please review settings and check if your models:\n- Fit within the build volume\n- Are not all set as modifier meshes"
msgstr "Controleer de instellingen en zorg ervoor dat uw modellen:"
"- binnen het werkvolume passen"
"- zijn toegewezen aan een ingeschakelde extruder"
"- niet allemaal zijn ingesteld als modificatierasters"
msgctxt "@label"

View file

@ -6289,8 +6289,8 @@ msgctxt "material_max_flowrate description"
msgid "Maximum flow rate that the printer can extrude for the material"
msgstr "Maximale stroomsnelheid die de printer voor het materiaal kan extruderen"
msgctxt "multi_material_paint_deepness label"
msgid "Multi-material Deepness"
msgctxt "multi_material_paint_depth label"
msgid "Multi-material Depth"
msgstr "Multi-materiële diepte"
msgctxt "multi_material_paint_resolution label"
@ -6309,8 +6309,8 @@ msgctxt "machine_scan_first_layer label"
msgid "Scan the first layer"
msgstr "Scan de eerste laag"
msgctxt "multi_material_paint_deepness description"
msgid "The deepness of the painted details inside the model. A higher deepness will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated deepness may vary."
msgctxt "multi_material_paint_depth description"
msgid "The depth of the painted details inside the model. A higher depth will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated depth may vary."
msgstr "De diepte van de geverfde details in het model. Een hogere diepte zorgt voor een betere interlocking, maar verhoogt de snijtijd en het geheugen. Stel een zeer hoge waarde in om zo diep mogelijk te gaan. De werkelijk berekende diepte kan variëren."
msgctxt "build_volume_fan_speed description"

View file

@ -3138,7 +3138,6 @@ msgctxt "@info:status"
msgid ""
"Please review settings and check if your models:\n"
"- Fit within the build volume\n"
"- Are assigned to an enabled extruder\n"
"- Are not all set as modifier meshes"
msgstr ""

View file

@ -2566,8 +2566,8 @@ msgctxt "skin_monotonic label"
msgid "Monotonic Top/Bottom Order"
msgstr ""
msgctxt "multi_material_paint_deepness label"
msgid "Multi-material Deepness"
msgctxt "multi_material_paint_depth label"
msgid "Multi-material Depth"
msgstr ""
msgctxt "multi_material_paint_resolution label"
@ -4330,8 +4330,8 @@ msgctxt "material_brand description"
msgid "The brand of material used."
msgstr ""
msgctxt "multi_material_paint_deepness description"
msgid "The deepness of the painted details inside the model. A higher deepness will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated deepness may vary."
msgctxt "multi_material_paint_depth description"
msgid "The depth of the painted details inside the model. A higher depth will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated depth may vary."
msgstr ""
msgctxt "machine_acceleration description"

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -3037,10 +3037,9 @@ msgid "Please remove the print"
msgstr "Remova a impressão"
msgctxt "@info:status"
msgid "Please review settings and check if your models:\n- Fit within the build volume\n- Are assigned to an enabled extruder\n- Are not all set as modifier meshes"
msgid "Please review settings and check if your models:\n- Fit within the build volume\n- Are not all set as modifier meshes"
msgstr "Reveja as definições e verifique se os seus modelos:"
"- Cabem dentro do volume de construção"
"- Estão atribuídos a uma extrusora ativada"
"- Não estão todos definidos como objetos modificadores"
msgctxt "@label"

View file

@ -6290,8 +6290,8 @@ msgctxt "material_max_flowrate description"
msgid "Maximum flow rate that the printer can extrude for the material"
msgstr "A taxa de fluxo máxima que a impressora pode extrudir para o material"
msgctxt "multi_material_paint_deepness label"
msgid "Multi-material Deepness"
msgctxt "multi_material_paint_depth label"
msgid "Multi-material Depth"
msgstr "Profundidade em múltiplos materiais"
msgctxt "multi_material_paint_resolution label"
@ -6310,8 +6310,8 @@ msgctxt "machine_scan_first_layer label"
msgid "Scan the first layer"
msgstr "Fazer a varredura da primeira camada"
msgctxt "multi_material_paint_deepness description"
msgid "The deepness of the painted details inside the model. A higher deepness will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated deepness may vary."
msgctxt "multi_material_paint_depth description"
msgid "The depth of the painted details inside the model. A higher depth will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated depth may vary."
msgstr "A profundidade dos detalhes pintados no interior do modelo. Uma profundidade maior proporcionará uma melhor interligação, mas aumentará o tempo de corte e a memória. Defina um valor muito alto para obter a maior profundidade possível. A profundidade calculada efetiva pode variar."
msgctxt "build_volume_fan_speed description"

View file

@ -3048,10 +3048,9 @@ msgid "Please remove the print"
msgstr "Пожалуйста, удалите напечатанное"
msgctxt "@info:status"
msgid "Please review settings and check if your models:\n- Fit within the build volume\n- Are assigned to an enabled extruder\n- Are not all set as modifier meshes"
msgid "Please review settings and check if your models:\n- Fit within the build volume\n- Are not all set as modifier meshes"
msgstr "Проверьте настройки и убедитесь в том, что ваши модели:"
"- соответствуют допустимой области печати"
"- назначены активированному экструдеру"
"- не заданы как объекты-модификаторы"
msgctxt "@label"

View file

@ -6289,8 +6289,8 @@ msgctxt "material_max_flowrate description"
msgid "Maximum flow rate that the printer can extrude for the material"
msgstr "Максимальная скорость подачи, с которой принтер может выдавливать материал"
msgctxt "multi_material_paint_deepness label"
msgid "Multi-material Deepness"
msgctxt "multi_material_paint_depth label"
msgid "Multi-material Depth"
msgstr "Мультиматериальная глубина"
msgctxt "multi_material_paint_resolution label"
@ -6309,8 +6309,8 @@ msgctxt "machine_scan_first_layer label"
msgid "Scan the first layer"
msgstr "Сканировать первый слой"
msgctxt "multi_material_paint_deepness description"
msgid "The deepness of the painted details inside the model. A higher deepness will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated deepness may vary."
msgctxt "multi_material_paint_depth description"
msgid "The depth of the painted details inside the model. A higher depth will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated depth may vary."
msgstr "Глубина покрашенных деталей внутри модели. Чем больше глубина, тем лучше сцепление, но при этом увеличивается время нарезки и объем памяти. Установите очень высокое значение, чтобы глубина была максимальной. Фактически рассчитанная глубина может отличаться."
msgctxt "build_volume_fan_speed description"

View file

@ -3036,10 +3036,9 @@ msgid "Please remove the print"
msgstr "Lütfen yazıcıyı çıkarın"
msgctxt "@info:status"
msgid "Please review settings and check if your models:\n- Fit within the build volume\n- Are assigned to an enabled extruder\n- Are not all set as modifier meshes"
msgid "Please review settings and check if your models:\n- Fit within the build volume\n- Are not all set as modifier meshes"
msgstr "Lütfen ayarları gözden geçirin ve modellerinizi şu durumlara karşı kontrol edin:"
"- Yapı hacmine sığma"
"- Etkin bir ekstrüdere atanma"
"- Değiştirici kafesler olarak ayarlanmama"
msgctxt "@label"

View file

@ -6289,8 +6289,8 @@ msgctxt "material_max_flowrate description"
msgid "Maximum flow rate that the printer can extrude for the material"
msgstr "Materyal için yazıcının sıkabileceği maksimum akış oranı"
msgctxt "multi_material_paint_deepness label"
msgid "Multi-material Deepness"
msgctxt "multi_material_paint_depth label"
msgid "Multi-material Depth"
msgstr "Çoklu Materyal Derinliği"
msgctxt "multi_material_paint_resolution label"
@ -6309,8 +6309,8 @@ msgctxt "machine_scan_first_layer label"
msgid "Scan the first layer"
msgstr "İlk katmanı tara"
msgctxt "multi_material_paint_deepness description"
msgid "The deepness of the painted details inside the model. A higher deepness will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated deepness may vary."
msgctxt "multi_material_paint_depth description"
msgid "The depth of the painted details inside the model. A higher depth will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated depth may vary."
msgstr "Model içerisindeki boyalı detayların derinliği. Daha yüksek bir derinlik, daha iyi bir kenetlenme sağlar, fakat dilimleme süresini ve bellek kullanımını artırır. Mümkün olduğunca derinleştirmek için çok yüksek bir değer seçin. Asıl hesaplanan derinlik farklılık gösterebilir."
msgctxt "build_volume_fan_speed description"

View file

@ -3030,10 +3030,9 @@ msgid "Please remove the print"
msgstr "请取出打印件"
msgctxt "@info:status"
msgid "Please review settings and check if your models:\n- Fit within the build volume\n- Are assigned to an enabled extruder\n- Are not all set as modifier meshes"
msgid "Please review settings and check if your models:\n- Fit within the build volume\n- Are not all set as modifier meshes"
msgstr "请检查设置并检查您的模型是否:"
"- 适合构建体积"
"- 分配给了已启用的挤出器"
"- 尚未全部设置为修改器网格"
msgctxt "@label"

View file

@ -6289,8 +6289,8 @@ msgctxt "material_max_flowrate description"
msgid "Maximum flow rate that the printer can extrude for the material"
msgstr "打印机可挤出该材料的最大流量率"
msgctxt "multi_material_paint_deepness label"
msgid "Multi-material Deepness"
msgctxt "multi_material_paint_depth label"
msgid "Multi-material Depth"
msgstr "多材料打印深度"
msgctxt "multi_material_paint_resolution label"
@ -6309,8 +6309,8 @@ msgctxt "machine_scan_first_layer label"
msgid "Scan the first layer"
msgstr "扫描首层"
msgctxt "multi_material_paint_deepness description"
msgid "The deepness of the painted details inside the model. A higher deepness will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated deepness may vary."
msgctxt "multi_material_paint_depth description"
msgid "The depth of the painted details inside the model. A higher depth will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated depth may vary."
msgstr "模型内部喷涂细节的深度。较高的深度值可提供更好的咬合效果,但会增加切片时间和内存占用。设置极高的数值可实现最大深度(实际计算深度可能存在浮动)。"
msgctxt "build_volume_fan_speed description"

View file

@ -3135,12 +3135,10 @@ msgctxt "@info:status"
msgid ""
"Please review settings and check if your models:\n"
"- Fit within the build volume\n"
"- Are assigned to an enabled extruder\n"
"- Are not all set as modifier meshes"
msgstr ""
"請檢查設定並檢查你的模型是否:\n"
"- 適合列印範圍\n"
"- 分配了一個已啟用的擠出機\n"
"- 沒有全部設定成修改網格"
msgctxt "@label"

View file

@ -2567,8 +2567,8 @@ msgctxt "skin_monotonic label"
msgid "Monotonic Top/Bottom Order"
msgstr "單一化列印 頂層/底層 順序"
msgctxt "multi_material_paint_deepness label"
msgid "Multi-material Deepness"
msgctxt "multi_material_paint_depth label"
msgid "Multi-material Depth"
msgstr ""
msgctxt "multi_material_paint_resolution label"
@ -4331,8 +4331,8 @@ msgctxt "material_brand description"
msgid "The brand of material used."
msgstr ""
msgctxt "multi_material_paint_deepness description"
msgid "The deepness of the painted details inside the model. A higher deepness will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated deepness may vary."
msgctxt "multi_material_paint_depth description"
msgid "The depth of the painted details inside the model. A higher depth will provide a better interlocking, but increase slicing time and memory. Set a very high value to go as deep as possible. The actually calculated depth may vary."
msgstr ""
msgctxt "machine_acceleration description"

View file

@ -19,6 +19,7 @@ Column
spacing: UM.Theme.getSize("thin_margin").height
property bool preSlicedData: PrintInformation.preSliced
property bool userTimeAdjusted: PrintInformation.userTimeAdjusted
property alias hasPreviewButton: previewStageShortcut.visible
UM.I18nCatalog
@ -54,9 +55,22 @@ Column
Cura.IconWithText
{
id: estimatedTime
width: parent.width
width: parent.width - printInformationPanel.width
text:
{
if (preSlicedData)
{
return catalog.i18nc("@label", "No time estimation available")
}
text: preSlicedData ? catalog.i18nc("@label", "No time estimation available") : PrintInformation.currentPrintTime.getDisplayString(UM.DurationFormat.Long)
if (userTimeAdjusted)
{
return PrintInformation.currentPrintTime.getDisplayString(UM.DurationFormat.Long) + " " + catalog.i18nc("@label", "*User Adjusted")
}
return PrintInformation.currentPrintTime.getDisplayString(UM.DurationFormat.Long)
}
source: UM.Theme.getIcon("Clock")
font: UM.Theme.getFont("medium_bold")
}

View file

@ -232,7 +232,7 @@ Item
Action
{
id: updateProfileAction
enabled: !Cura.MachineManager.stacksHaveErrors && Cura.MachineManager.hasUserSettings && Cura.MachineManager.activeQualityChangesGroup != null
enabled: !Cura.MachineErrorChecker.hasError && Cura.MachineManager.hasUserSettings && Cura.MachineManager.activeQualityChangesGroup != null
text: catalog.i18nc("@action:inmenu menubar:profile", "&Update profile with current settings/overrides");
onTriggered: Cura.ContainerManager.updateQualityChanges()
}
@ -252,7 +252,7 @@ Item
Action
{
id: addProfileAction
enabled: !Cura.MachineManager.stacksHaveErrors && Cura.MachineManager.hasUserSettings
enabled: !Cura.MachineErrorChecker.hasError && Cura.MachineManager.hasUserSettings
text: catalog.i18nc("@action:inmenu menubar:profile", "&Create profile from current settings/overrides...")
}

View file

@ -93,7 +93,7 @@ UM.ManagementPage
id: createMenuButton
text: catalog.i18nc("@action:button", "Create new")
enabled: !Cura.MachineManager.stacksHaveErrors
enabled: !Cura.MachineErrorChecker.hasError
visible: base.canCreateProfile
tooltip: catalog.i18nc("@action:tooltip", "Create new profile from current settings/overrides")
onClicked:

View file

@ -258,6 +258,10 @@ Item
border.width: UM.Theme.getSize("default_lining").width
color: UM.Theme.getColor("main_background")
// Reduce opacity of the settings when the selected extruder is disabled
opacity: Cura.MachineManager.activeStack != null && !Cura.MachineManager.activeStack.isEnabled ? 0.4 : 1.0
Cura.SettingView
{
anchors

View file

@ -30,7 +30,6 @@ speed_print = 60.0
speed_travel_layer_0 = 125.0
speed_wall_x = 50.0
top_bottom_thickness = =layer_height_0+layer_height*3
top_layers = =0 if infill_sparse_density == 100 else math.ceil(round(top_thickness / resolveOrValue('layer_height'), 4))
wall_0_material_flow = 95
wall_0_wipe_dist = 0.4
wall_thickness = 1.2

View file

@ -30,7 +30,6 @@ speed_print = 60.0
speed_travel_layer_0 = 125.0
speed_wall_x = 50.0
top_bottom_thickness = =layer_height_0+layer_height*3
top_layers = =0 if infill_sparse_density == 100 else math.ceil(round(top_thickness / resolveOrValue('layer_height'), 4))
wall_0_material_flow = 95
wall_0_wipe_dist = 0.4
wall_thickness = 1.2

View file

@ -109,7 +109,6 @@ support_xy_distance = 0.5
support_xy_overrides_z = xy_overrides_z
support_z_distance = 0.2
top_bottom_thickness = 0.6
top_layers = =0 if infill_sparse_density == 100 else math.ceil(round(top_thickness / resolveOrValue('layer_height'), 4))
top_thickness = 1
travel_avoid_other_parts = True
travel_avoid_supports = True

View file

@ -16,7 +16,7 @@ cool_min_temperature = =material_print_temperature - 20
hole_xy_offset = 0.1
material_final_print_temperature = =material_print_temperature - 15
material_initial_print_temperature = =material_print_temperature - 15
retraction_prime_speed = =retraction_speed
retraction_prime_speed = 30
speed_roofing = =speed_topbottom * 1/3
speed_wall_x = =speed_wall
speed_wall_x_roofing = =speed_wall * 0.8

View file

@ -16,7 +16,7 @@ cool_min_temperature = =material_print_temperature - 20
hole_xy_offset = 0.1
material_final_print_temperature = =material_print_temperature - 15
material_initial_print_temperature = =material_print_temperature - 15
retraction_prime_speed = =retraction_speed
retraction_prime_speed = 30
speed_roofing = =speed_topbottom * 1/3
speed_wall_x = =speed_wall
speed_wall_x_roofing = =speed_wall * 0.8

View file

@ -16,7 +16,7 @@ cool_min_temperature = =material_print_temperature - 20
hole_xy_offset = 0.1
material_final_print_temperature = =material_print_temperature - 15
material_initial_print_temperature = =material_print_temperature - 15
retraction_prime_speed = =retraction_speed
retraction_prime_speed = 30
speed_roofing = =speed_topbottom * 1/3
speed_wall_x = =speed_wall
speed_wall_x_roofing = =speed_wall * 0.8

View file

@ -14,7 +14,7 @@ weight = -1
[values]
cool_min_temperature = =material_print_temperature - 20
hole_xy_offset = 0.1
retraction_prime_speed = =retraction_speed
retraction_prime_speed = 30
retraction_speed = 25
speed_roofing = =speed_topbottom * 1/3
speed_wall_x = =speed_wall

View file

@ -14,7 +14,7 @@ weight = 0
[values]
cool_min_temperature = =material_print_temperature - 20
hole_xy_offset = 0.1
retraction_prime_speed = =retraction_speed
retraction_prime_speed = 30
speed_roofing = =speed_topbottom * 1/3
speed_wall_x = =speed_wall
speed_wall_x_roofing = =speed_wall * 0.8

View file

@ -14,7 +14,7 @@ weight = -2
[values]
cool_min_temperature = =material_print_temperature - 20
hole_xy_offset = 0.1
retraction_prime_speed = =retraction_speed
retraction_prime_speed = 30
speed_roofing = =speed_topbottom * 1/3
speed_wall_x = =speed_wall
speed_wall_x_roofing = =speed_wall * 0.8

View file

@ -137,7 +137,7 @@ prime_tower_position_x
prime_tower_position_y
prime_tower_brim_enable
interlocking_enable
multi_material_paint_deepness
multi_material_paint_depth
multi_material_paint_resolution
[meshfix]

Some files were not shown because too many files have changed in this diff Show more