diff --git a/.github/workflows/find-packages.yml b/.github/workflows/find-packages.yml new file mode 100644 index 0000000000..93a5bdde2b --- /dev/null +++ b/.github/workflows/find-packages.yml @@ -0,0 +1,51 @@ +name: All installers (based on Jira ticket) +run-name: ${{ inputs.jira_ticket_number }} by @${{ github.actor }} + +on: + workflow_dispatch: + inputs: + jira_ticket_number: + description: 'Jira ticket number (e.g. CURA-15432 or cura_12345)' + required: true + type: string + start_builds: + description: 'Start installers build based on found packages' + default: true + required: false + type: boolean + conan_args: + description: 'Conan args' + default: '' + type: string + enterprise: + description: 'Build Cura as an Enterprise edition' + default: false + type: boolean + staging: + description: 'Use staging API' + default: false + type: boolean + +permissions: + contents: read + +jobs: + find-packages: + name: Find packages for Jira ticket + uses: ultimaker/cura-workflows/.github/workflows/find-package-by-ticket.yml@main + with: + jira_ticket_number: ${{ inputs.jira_ticket_number }} + secrets: inherit + + installers: + name: Create installers + needs: find-packages + 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 }} + conan_args: ${{ inputs.conan_args }} + enterprise: ${{ inputs.enterprise }} + staging: ${{ inputs.staging }} + secrets: inherit diff --git a/.github/workflows/printer-linter-pr-diagnose.yml b/.github/workflows/printer-linter-pr-diagnose.yml index 64892e0db1..666383c8f9 100644 --- a/.github/workflows/printer-linter-pr-diagnose.yml +++ b/.github/workflows/printer-linter-pr-diagnose.yml @@ -47,7 +47,7 @@ jobs: path: printer-linter-result/ - name: Run clang-tidy-pr-comments action - uses: platisd/clang-tidy-pr-comments@bc0bb7da034a8317d54e7fe1e819159002f4cc40 + uses: platisd/clang-tidy-pr-comments@v1.8.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} clang_tidy_fixes: result.yml diff --git a/conandata.yml b/conandata.yml index 3181d601e5..924082efea 100644 --- a/conandata.yml +++ b/conandata.yml @@ -1,4 +1,5 @@ version: "5.11.0-alpha.0" +commit: "unknown" requirements: - "cura_resources/5.11.0-alpha.0@ultimaker/testing" - "uranium/5.11.0-alpha.0@ultimaker/testing" @@ -99,6 +100,7 @@ pyinstaller: - "pyArcus" - "pyDulcificum" - "pynest2d" + - "pyUvula" - "PyQt6" - "PyQt6.QtNetwork" - "PyQt6.sip" diff --git a/conanfile.py b/conanfile.py index 28f45e7c24..dbe524b2f1 100644 --- a/conanfile.py +++ b/conanfile.py @@ -16,7 +16,7 @@ from conan import ConanFile from conan.tools.files import copy, rmdir, save, mkdir, rm, update_conandata from conan.tools.microsoft import unix_path from conan.tools.env import VirtualRunEnv, Environment, VirtualBuildEnv -from conan.tools.scm import Version +from conan.tools.scm import Version, Git from conan.errors import ConanInvalidConfiguration, ConanException required_conan_version = ">=2.7.0" # When changing the version, also change the one in conandata.yml/extra_dependencies @@ -329,10 +329,16 @@ class CuraConan(ConanFile): # If you want a specific Cura version to show up on the splash screen add the user configuration `user.cura:version=VERSION` # the global.conf, profile, package_info (of dependency) or via the cmd line `-c user.cura:version=VERSION` cura_version = Version(self.conf.get("user.cura:version", default = self.version, check_type = str)) - pre_tag = f"-{cura_version.pre}" if cura_version.pre else "" - build_tag = f"+{cura_version.build}" if cura_version.build else "" - internal_tag = f"+internal" if self.options.internal else "" - cura_version = f"{cura_version.major}.{cura_version.minor}.{cura_version.patch}{pre_tag}{build_tag}{internal_tag}" + extra_build_identifiers = [] + + if self.options.internal: + extra_build_identifiers.append("internal") + if str(cura_version.pre).startswith("alpha") and self.conan_data["commit"] != "unknown": + extra_build_identifiers.append(self.conan_data["commit"][:6]) + + if extra_build_identifiers: + separator = "+" if not cura_version.build else "." + cura_version = Version(f"{cura_version}{separator}{'.'.join(extra_build_identifiers)}") self.output.info(f"Write CuraVersion.py to {self.recipe_folder}") @@ -340,7 +346,7 @@ class CuraConan(ConanFile): f.write(cura_version_py.render( cura_app_name = self.name, cura_app_display_name = self._app_name, - cura_version = cura_version, + cura_version = str(cura_version), cura_version_full = self.version, cura_build_type = "Enterprise" if self.options.enterprise else "", cura_debug_mode = self.options.cura_debug_mode, @@ -527,7 +533,7 @@ class CuraConan(ConanFile): )) def export(self): - update_conandata(self, {"version": self.version}) + update_conandata(self, {"version": self.version, "commit": Git(self).get_commit()}) def export_sources(self): copy(self, "*", os.path.join(self.recipe_folder, "plugins"), os.path.join(self.export_sources_folder, "plugins")) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 8af98c2d0e..57d4773cb3 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -9,7 +9,6 @@ import time import platform from pathlib import Path from typing import cast, TYPE_CHECKING, Optional, Callable, List, Any, Dict -import requests import numpy from PyQt6.QtCore import QObject, QTimer, QUrl, QUrlQuery, pyqtSignal, pyqtProperty, QEvent, pyqtEnum, QCoreApplication, \ @@ -60,6 +59,7 @@ from cura import ApplicationMetadata from cura.API import CuraAPI from cura.API.Account import Account from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob +from cura.CuraRenderer import CuraRenderer from cura.Machines.MachineErrorChecker import MachineErrorChecker from cura.Machines.Models.BuildPlateModel import BuildPlateModel from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel @@ -362,6 +362,9 @@ class CuraApplication(QtApplication): self._machine_action_manager = MachineActionManager(self) self._machine_action_manager.initialize() + def makeRenderer(self) -> CuraRenderer: + return CuraRenderer(self) + def __sendCommandToSingleInstance(self): self._single_instance = SingleInstance(self, self._files_to_open, self._urls_to_open) @@ -1035,7 +1038,6 @@ class CuraApplication(QtApplication): # Initialize UI state controller.setActiveStage("PrepareStage") - controller.setActiveView("SolidView") controller.setCameraTool("CameraTool") controller.setSelectionTool("SelectionTool") @@ -1645,14 +1647,10 @@ class CuraApplication(QtApplication): Logger.log("w", "Unable to reload data because we don't have a filename.") for file_name, nodes in objects_in_filename.items(): - file_path = os.path.normpath(os.path.dirname(file_name)) - job = ReadMeshJob(file_name, - add_to_recent_files=file_path != tempfile.gettempdir()) # Don't add temp files to the recent files list - job._nodes = nodes # type: ignore - job.finished.connect(self._reloadMeshFinished) + on_done = None if has_merged_nodes: - job.finished.connect(self.updateOriginOfMergedMeshes) - job.start() + on_done = self.updateOriginOfMergedMeshes + self.getController().getScene().reloadNodes(nodes, file_name, on_done) @pyqtSlot("QStringList") def setExpandedCategories(self, categories: List[str]) -> None: @@ -1835,53 +1833,6 @@ class CuraApplication(QtApplication): fileLoaded = pyqtSignal(str) fileCompleted = pyqtSignal(str) - def _reloadMeshFinished(self, job) -> None: - """ - Function called when ReadMeshJob finishes reloading a file in the background, then update node objects in the - scene from its source file. The function gets all the nodes that exist in the file through the job result, and - then finds the scene nodes that need to be refreshed by their name. Each job refreshes all nodes of a file. - Nodes that are not present in the updated file are kept in the scene. - - :param job: The :py:class:`Uranium.UM.ReadMeshJob.ReadMeshJob` running in the background that reads all the - meshes in a file - """ - - job_result = job.getResult() # nodes that exist inside the file read by this job - if len(job_result) == 0: - Logger.log("e", "Reloading the mesh failed.") - return - renamed_nodes = {} # type: Dict[str, int] - # Find the node to be refreshed based on its id - for job_result_node in job_result: - mesh_data = job_result_node.getMeshData() - if not mesh_data: - Logger.log("w", "Could not find a mesh in reloaded node.") - continue - - # Solves issues with object naming - result_node_name = job_result_node.getName() - if not result_node_name: - result_node_name = os.path.basename(mesh_data.getFileName()) - if result_node_name in renamed_nodes: # objects may get renamed by ObjectsModel._renameNodes() when loaded - renamed_nodes[result_node_name] += 1 - result_node_name = "{0}({1})".format(result_node_name, renamed_nodes[result_node_name]) - else: - renamed_nodes[job_result_node.getName()] = 0 - - # Find the matching scene node to replace - scene_node = None - for replaced_node in job._nodes: - if replaced_node.getName() == result_node_name: - scene_node = replaced_node - break - - if scene_node: - scene_node.setMeshData(mesh_data) - else: - # Current node is a new one in the file, or it's name has changed - # TODO: Load this mesh into the scene. Also alter the "_reloadJobFinished" action in UM.Scene - Logger.log("w", "Could not find matching node for object '{0}' in the scene.".format(result_node_name)) - def _openFile(self, filename): self.readLocalFile(QUrl.fromLocalFile(filename)) @@ -2137,9 +2088,7 @@ class CuraApplication(QtApplication): is_non_sliceable = "." + file_extension in self._non_sliceable_extensions if is_non_sliceable: - # Need to switch first to the preview stage and then to layer view - self.callLater(lambda: (self.getController().setActiveStage("PreviewStage"), - self.getController().setActiveView("SimulationView"))) + self.callLater(lambda: (self.getController().setActiveStage("PreviewStage"))) block_slicing_decorator = BlockSlicingDecorator() node.addDecorator(block_slicing_decorator) diff --git a/cura/CuraRenderer.py b/cura/CuraRenderer.py new file mode 100644 index 0000000000..77030b3fe8 --- /dev/null +++ b/cura/CuraRenderer.py @@ -0,0 +1,46 @@ +# Copyright (c) 2025 UltiMaker +# Uranium is released under the terms of the LGPLv3 or higher. + + +from typing import TYPE_CHECKING + +from cura.PickingPass import PickingPass +from UM.Qt.QtRenderer import QtRenderer +from UM.View.RenderPass import RenderPass +from UM.View.SelectionPass import SelectionPass + +if TYPE_CHECKING: + from cura.CuraApplication import CuraApplication + + +class CuraRenderer(QtRenderer): + """An overridden Renderer implementation that adds some behaviors specific to Cura.""" + + def __init__(self, application: "CuraApplication") -> None: + super().__init__() + + self._controller = application.getController() + self._controller.activeToolChanged.connect(self._onActiveToolChanged) + self._extra_rendering_passes: list[RenderPass] = [] + + def _onActiveToolChanged(self) -> None: + tool_extra_rendering_passes = [] + + active_tool = self._controller.getActiveTool() + if active_tool is not None: + tool_extra_rendering_passes = active_tool.getRequiredExtraRenderingPasses() + + for extra_rendering_pass in self._extra_rendering_passes: + extra_rendering_pass.setEnabled(extra_rendering_pass.getName() in tool_extra_rendering_passes) + + def _makeRenderPasses(self) -> list[RenderPass]: + self._extra_rendering_passes = [ + SelectionPass(self._viewport_width, self._viewport_height, SelectionPass.SelectionMode.FACES), + PickingPass(self._viewport_width, self._viewport_height, only_selected_objects=True), + PickingPass(self._viewport_width, self._viewport_height, only_selected_objects=False) + ] + + for extra_rendering_pass in self._extra_rendering_passes: + extra_rendering_pass.setEnabled(False) + + return super()._makeRenderPasses() + self._extra_rendering_passes diff --git a/cura/LayerDataBuilder.py b/cura/LayerDataBuilder.py index d8801c9e7b..ff80307223 100755 --- a/cura/LayerDataBuilder.py +++ b/cura/LayerDataBuilder.py @@ -80,9 +80,13 @@ class LayerDataBuilder(MeshBuilder): material_colors = numpy.zeros((line_dimensions.shape[0], 4), dtype=numpy.float32) for extruder_nr in range(material_color_map.shape[0]): material_colors[extruders == extruder_nr] = material_color_map[extruder_nr] - # Set material_colors with indices where line_types (also numpy array) == MoveCombingType - material_colors[line_types == LayerPolygon.MoveCombingType] = colors[line_types == LayerPolygon.MoveCombingType] - material_colors[line_types == LayerPolygon.MoveRetractionType] = colors[line_types == LayerPolygon.MoveRetractionType] + # Set material_colors with indices where line_types (also numpy array) == MoveUnretractedType + material_colors[line_types == LayerPolygon.MoveUnretractedType] = colors[line_types == LayerPolygon.MoveUnretractedType] + material_colors[line_types == LayerPolygon.MoveRetractedType] = colors[line_types == LayerPolygon.MoveRetractedType] + material_colors[line_types == LayerPolygon.MoveWhileRetractingType] = colors[ + line_types == LayerPolygon.MoveWhileRetractingType] + material_colors[line_types == LayerPolygon.MoveWhileUnretractingType] = colors[ + line_types == LayerPolygon.MoveWhileUnretractingType] attributes = { "line_dimensions": { diff --git a/cura/LayerPolygon.py b/cura/LayerPolygon.py index e772a8b78e..cd4642d719 100644 --- a/cura/LayerPolygon.py +++ b/cura/LayerPolygon.py @@ -19,15 +19,22 @@ class LayerPolygon: SkirtType = 5 InfillType = 6 SupportInfillType = 7 - MoveCombingType = 8 - MoveRetractionType = 9 + MoveUnretractedType = 8 + MoveRetractedType = 9 SupportInterfaceType = 10 PrimeTowerType = 11 - __number_of_types = 12 + MoveWhileRetractingType = 12 + MoveWhileUnretractingType = 13 + StationaryRetractUnretract = 14 + __number_of_types = 15 - __jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, - numpy.arange(__number_of_types) == MoveCombingType), - numpy.arange(__number_of_types) == MoveRetractionType) + __jump_map = numpy.logical_or(numpy.logical_or(numpy.logical_or( + numpy.arange(__number_of_types) == NoneType, + numpy.arange(__number_of_types) == MoveUnretractedType), + numpy.logical_or( + numpy.arange(__number_of_types) == MoveRetractedType, + numpy.arange(__number_of_types) == MoveWhileRetractingType)), + numpy.arange(__number_of_types) == MoveWhileUnretractingType) def __init__(self, extruder: int, line_types: numpy.ndarray, data: numpy.ndarray, line_widths: numpy.ndarray, line_thicknesses: numpy.ndarray, line_feedrates: numpy.ndarray) -> None: @@ -269,10 +276,13 @@ class LayerPolygon: theme.getColor("layerview_skirt").getRgbF(), # SkirtType theme.getColor("layerview_infill").getRgbF(), # InfillType theme.getColor("layerview_support_infill").getRgbF(), # SupportInfillType - theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType - theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractionType + theme.getColor("layerview_move_combing").getRgbF(), # MoveUnretractedType + theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractedType theme.getColor("layerview_support_interface").getRgbF(), # SupportInterfaceType - theme.getColor("layerview_prime_tower").getRgbF() # PrimeTowerType + theme.getColor("layerview_prime_tower").getRgbF(), # PrimeTowerType + theme.getColor("layerview_move_while_retracting").getRgbF(), # MoveWhileRetracting + theme.getColor("layerview_move_while_unretracting").getRgbF(), # MoveWhileUnretracting + theme.getColor("layerview_move_retraction").getRgbF(), # StationaryRetractUnretract ]) return cls.__color_map diff --git a/cura/Machines/MachineErrorChecker.py b/cura/Machines/MachineErrorChecker.py index 5edee0778f..87d50e46d4 100644 --- a/cura/Machines/MachineErrorChecker.py +++ b/cura/Machines/MachineErrorChecker.py @@ -61,6 +61,7 @@ class MachineErrorChecker(QObject): self._machine_manager.globalContainerChanged.connect(self.startErrorCheck) self._onMachineChanged() + self.startErrorCheck() def _setCheckTimer(self) -> None: """A QTimer to regulate error check frequency diff --git a/cura/PickingPass.py b/cura/PickingPass.py index 4d6ef671df..e585e72269 100644 --- a/cura/PickingPass.py +++ b/cura/PickingPass.py @@ -7,6 +7,7 @@ from UM.Qt.QtApplication import QtApplication from UM.Logger import Logger from UM.Math.Vector import Vector from UM.Resources import Resources +from UM.Scene.Selection import Selection from UM.View.RenderPass import RenderPass from UM.View.GL.OpenGL import OpenGL @@ -27,13 +28,14 @@ class PickingPass(RenderPass): .. note:: that in order to increase precision, the 24 bit depth value is encoded into all three of the R,G & B channels """ - def __init__(self, width: int, height: int) -> None: - super().__init__("picking", width, height) + def __init__(self, width: int, height: int, only_selected_objects: bool = False) -> None: + super().__init__("picking" if not only_selected_objects else "picking_selected", width, height) self._renderer = QtApplication.getInstance().getRenderer() self._shader = None #type: Optional[ShaderProgram] self._scene = QtApplication.getInstance().getController().getScene() + self._only_selected_objects = only_selected_objects def render(self) -> None: if not self._shader: @@ -53,7 +55,7 @@ class PickingPass(RenderPass): # Fill up the batch with objects that can be sliced. ` for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. - if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible(): + if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible() and (not self._only_selected_objects or Selection.isSelected(node)): batch.addItem(node.getWorldTransformation(copy = False), node.getMeshData(), normal_transformation=node.getCachedNormalMatrix()) self.bind() diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 3dc245d468..1d0be1389e 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -33,8 +33,8 @@ class AuthState(IntEnum): class NetworkedPrinterOutputDevice(PrinterOutputDevice): authenticationStateChanged = pyqtSignal() - def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], connection_type: ConnectionType = ConnectionType.NetworkConnection, parent: QObject = None) -> None: - super().__init__(device_id = device_id, connection_type = connection_type, parent = parent) + def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], connection_type: ConnectionType = ConnectionType.NetworkConnection, parent: QObject = None, active: bool = True) -> None: + super().__init__(device_id = device_id, connection_type = connection_type, parent = parent, active = active) self._manager = None # type: Optional[QNetworkAccessManager] self._timeout_time = 10 # After how many seconds of no response should a timeout occur? diff --git a/cura/PrinterOutput/PrinterOutputDevice.py b/cura/PrinterOutput/PrinterOutputDevice.py index 9c1727f569..b369fc1129 100644 --- a/cura/PrinterOutput/PrinterOutputDevice.py +++ b/cura/PrinterOutput/PrinterOutputDevice.py @@ -72,7 +72,10 @@ class PrinterOutputDevice(QObject, OutputDevice): # Signal to indicate that the configuration of one of the printers has changed. uniqueConfigurationsChanged = pyqtSignal() - def __init__(self, device_id: str, connection_type: "ConnectionType" = ConnectionType.NotConnected, parent: QObject = None) -> None: + # Signal to indicate that the printer has become active or inactive + activeChanged = pyqtSignal() + + def __init__(self, device_id: str, connection_type: "ConnectionType" = ConnectionType.NotConnected, parent: QObject = None, active: bool = True) -> None: super().__init__(device_id = device_id, parent = parent) # type: ignore # MyPy complains with the multiple inheritance self._printers = [] # type: List[PrinterOutputModel] @@ -88,6 +91,8 @@ class PrinterOutputDevice(QObject, OutputDevice): self._accepts_commands = False # type: bool + self._active: bool = active + self._update_timer = QTimer() # type: QTimer self._update_timer.setInterval(2000) # TODO; Add preference for update interval self._update_timer.setSingleShot(False) @@ -295,3 +300,17 @@ class PrinterOutputDevice(QObject, OutputDevice): return self._firmware_updater.updateFirmware(firmware_file) + + @pyqtProperty(bool, notify = activeChanged) + def active(self) -> bool: + """ + Indicates whether the printer is active, which is not the same as "being the active printer". In this case, + active means that the printer can be used. An example of an inactive printer is one that cannot be used because + the user doesn't have enough seats on Digital Factory. + """ + return self._active + + def _setActive(self, active: bool) -> None: + if active != self._active: + self._active = active + self.activeChanged.emit() diff --git a/cura/Scene/SliceableObjectDecorator.py b/cura/Scene/SliceableObjectDecorator.py index ad51f7d755..7ee77795e7 100644 --- a/cura/Scene/SliceableObjectDecorator.py +++ b/cura/Scene/SliceableObjectDecorator.py @@ -1,12 +1,63 @@ +import copy +import json + +from typing import Optional, Dict + +from PyQt6.QtCore import QBuffer +from PyQt6.QtGui import QImage, QImageWriter + +import UM.View.GL.Texture from UM.Scene.SceneNodeDecorator import SceneNodeDecorator +from UM.View.GL.OpenGL import OpenGL +from UM.View.GL.Texture import Texture class SliceableObjectDecorator(SceneNodeDecorator): def __init__(self) -> None: super().__init__() + self._paint_texture = None + self._texture_data_mapping: Dict[str, tuple[int, int]] = {} def isSliceable(self) -> bool: return True + def getPaintTexture(self) -> Optional[Texture]: + return self._paint_texture + + def setPaintTexture(self, texture: Texture) -> None: + self._paint_texture = texture + + def getTextureDataMapping(self) -> Dict[str, tuple[int, int]]: + return self._texture_data_mapping + + def setTextureDataMapping(self, mapping: Dict[str, tuple[int, int]]) -> None: + self._texture_data_mapping = mapping + + def prepareTexture(self, width: int, height: int) -> None: + if self._paint_texture is None: + self._paint_texture = OpenGL.getInstance().createTexture(width, height) + image = QImage(width, height, QImage.Format.Format_RGB32) + image.fill(0) + self._paint_texture.setImage(image) + + def packTexture(self) -> Optional[bytearray]: + if self._paint_texture is None: + return None + + texture_image = self._paint_texture.getImage() + if texture_image is None: + return None + + texture_buffer = QBuffer() + texture_buffer.open(QBuffer.OpenModeFlag.ReadWrite) + image_writer = QImageWriter(texture_buffer, b"png") + image_writer.setText("Description", json.dumps(self._texture_data_mapping)) + image_writer.write(texture_image) + + return texture_buffer.data() + def __deepcopy__(self, memo) -> "SliceableObjectDecorator": - return type(self)() + copied_decorator = SliceableObjectDecorator() + copied_decorator.setPaintTexture(copy.deepcopy(self.getPaintTexture())) + copied_decorator.setTextureDataMapping(copy.deepcopy(self.getTextureDataMapping())) + return copied_decorator diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 986608cd49..3a2201449d 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -183,10 +183,14 @@ class MachineManager(QObject): self.setActiveMachine(active_machine_id) def _onOutputDevicesChanged(self) -> None: + for printer_output_device in self._printer_output_devices: + printer_output_device.activeChanged.disconnect(self.printerConnectedStatusChanged) + self._printer_output_devices = [] for printer_output_device in self._application.getOutputDeviceManager().getOutputDevices(): if isinstance(printer_output_device, PrinterOutputDevice): self._printer_output_devices.append(printer_output_device) + printer_output_device.activeChanged.connect(self.printerConnectedStatusChanged) self.outputDevicesChanged.emit() @@ -569,6 +573,13 @@ class MachineManager(QObject): def activeMachineIsUsingCloudConnection(self) -> bool: return self.activeMachineHasCloudConnection and not self.activeMachineHasNetworkConnection + @pyqtProperty(bool, notify = printerConnectedStatusChanged) + def activeMachineIsActive(self) -> bool: + if not self._printer_output_devices: + return True + + return self._printer_output_devices[0].active + def activeMachineNetworkKey(self) -> str: if self._global_container_stack: return self._global_container_stack.getMetaDataEntry("um_network_key", "") diff --git a/cura/Stages/CuraStage.py b/cura/Stages/CuraStage.py index 869ed309dc..8c207db8ad 100644 --- a/cura/Stages/CuraStage.py +++ b/cura/Stages/CuraStage.py @@ -1,6 +1,8 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import Optional + from PyQt6.QtCore import pyqtProperty, QUrl from UM.Stage import Stage @@ -13,8 +15,8 @@ from UM.Stage import Stage # * The MainComponent is the component that will be drawn starting from the bottom of the stageBar and fills the rest # of the screen. class CuraStage(Stage): - def __init__(self, parent = None) -> None: - super().__init__(parent) + def __init__(self, parent = None, active_view: Optional[str] = "SolidView") -> None: + super().__init__(parent, active_view = active_view) @pyqtProperty(str, constant = True) def stageId(self) -> str: diff --git a/cura/XRayPass.py b/cura/XRayPass.py index 965294ba89..20fe38741e 100644 --- a/cura/XRayPass.py +++ b/cura/XRayPass.py @@ -16,7 +16,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator class XRayPass(RenderPass): def __init__(self, width, height): - super().__init__("xray", width, height) + super().__init__("xray", width, height, -100) self._shader = None self._gl = OpenGL.getInstance().getBindingsObject() diff --git a/packaging/AppImage-builder/AppImageBuilder.yml.jinja b/packaging/AppImage-builder/AppImageBuilder.yml.jinja index c6e7a7123a..29144b0e77 100644 --- a/packaging/AppImage-builder/AppImageBuilder.yml.jinja +++ b/packaging/AppImage-builder/AppImageBuilder.yml.jinja @@ -77,3 +77,4 @@ AppImage: arch: {{ arch }} file_name: {{ file_name }} update-information: guess + comp: gzip diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index 45ab2e7d2f..09143dde64 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -1,12 +1,14 @@ # Copyright (c) 2021-2022 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. - +import json import os.path import zipfile from typing import List, Optional, Union, TYPE_CHECKING, cast import pySavitar as Savitar import numpy +from PyQt6.QtCore import QBuffer +from PyQt6.QtGui import QImage, QImageReader from UM.Logger import Logger from UM.Math.Matrix import Matrix @@ -18,6 +20,8 @@ from UM.Scene.GroupDecorator import GroupDecorator from UM.Scene.SceneNode import SceneNode # For typing. from UM.Scene.SceneNodeSettings import SceneNodeSettings from UM.Util import parseBool +from UM.View.GL.OpenGL import OpenGL +from UM.View.GL.Texture import Texture from cura.CuraApplication import CuraApplication from cura.Machines.ContainerTree import ContainerTree from cura.Scene.BuildPlateDecorator import BuildPlateDecorator @@ -94,14 +98,14 @@ class ThreeMFReader(MeshReader): return temp_mat @staticmethod - def _convertSavitarNodeToUMNode(savitar_node: Savitar.SceneNode, file_name: str = "", archive: zipfile.ZipFile = None) -> Optional[SceneNode]: + def _convertSavitarNodeToUMNode(savitar_node: Savitar.SceneNode, file_name: str = "", archive: zipfile.ZipFile = None, scene: Savitar.Scene = None) -> Optional[SceneNode]: """Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node. :returns: Scene node. """ try: node_name = savitar_node.getName() - node_id = savitar_node.getId() + node_id = str(savitar_node.getId()) except AttributeError: Logger.log("e", "Outdated version of libSavitar detected! Please update to the newest version!") node_name = "" @@ -131,23 +135,31 @@ class ThreeMFReader(MeshReader): um_node.setTransformation(transformation) mesh_builder = MeshBuilder() - data = numpy.fromstring(savitar_node.getMeshData().getFlatVerticesAsBytes(), dtype=numpy.float32) + mesh_data = savitar_node.getMeshData() + + vertices_data = numpy.fromstring(mesh_data.getFlatVerticesAsBytes(), dtype=numpy.float32) + vertices = numpy.resize(vertices_data, (int(vertices_data.size / 3), 3)) + + texture_path = mesh_data.getTexturePath(scene) + uv_data = numpy.fromstring(mesh_data.getUVCoordinatesPerVertexAsBytes(scene), dtype=numpy.float32) + uv_coordinates = numpy.resize(uv_data, (int(uv_data.size / 2), 2)) - vertices = numpy.resize(data, (int(data.size / 3), 3)) mesh_builder.setVertices(vertices) mesh_builder.calculateNormals(fast=True) mesh_builder.setMeshId(node_id) + mesh_builder.setUVCoordinates(uv_coordinates) if file_name: # The filename is used to give the user the option to reload the file if it is changed on disk # It is only set for the root node of the 3mf file mesh_builder.setFileName(file_name) + mesh_data = mesh_builder.build() if len(mesh_data.getVertices()): um_node.setMeshData(mesh_data) for child in savitar_node.getChildren(): - child_node = ThreeMFReader._convertSavitarNodeToUMNode(child, archive=archive) + child_node = ThreeMFReader._convertSavitarNodeToUMNode(child, archive=archive, scene=scene) if child_node: um_node.addChild(child_node) @@ -219,6 +231,30 @@ class ThreeMFReader(MeshReader): # affects (auto) slicing sliceable_decorator = SliceableObjectDecorator() um_node.addDecorator(sliceable_decorator) + + if texture_path != "" and archive is not None: + texture_data = archive.open(texture_path).read() + texture_buffer = QBuffer() + texture_buffer.open(QBuffer.OpenModeFlag.ReadWrite) + texture_buffer.write(texture_data) + + image_reader = QImageReader(texture_buffer, b"png") + + texture_buffer.seek(0) + texture_image = image_reader.read() + texture = Texture(OpenGL.getInstance()) + texture.setImage(texture_image) + sliceable_decorator.setPaintTexture(texture) + + texture_buffer.seek(0) + data_mapping_desc = image_reader.text("Description") + if data_mapping_desc != "": + data_mapping = json.loads(data_mapping_desc) + for key, value in data_mapping.items(): + # Tuples are stored as lists in json, restore them back to tuples + data_mapping[key] = tuple(value) + sliceable_decorator.setTextureDataMapping(data_mapping) + return um_node def _read(self, file_name: str) -> Union[SceneNode, List[SceneNode]]: @@ -236,7 +272,7 @@ class ThreeMFReader(MeshReader): CuraApplication.getInstance().getController().getScene().setMetaDataEntry(key, value) for node in scene_3mf.getSceneNodes(): - um_node = ThreeMFReader._convertSavitarNodeToUMNode(node, file_name, archive) + um_node = ThreeMFReader._convertSavitarNodeToUMNode(node, file_name, archive, scene_3mf) if um_node is None: continue @@ -336,7 +372,7 @@ class ThreeMFReader(MeshReader): # Convert the scene to scene nodes nodes = [] for savitar_node in scene.getSceneNodes(): - scene_node = ThreeMFReader._convertSavitarNodeToUMNode(savitar_node, "file_name") + scene_node = ThreeMFReader._convertSavitarNodeToUMNode(savitar_node, "file_name", scene=scene) if scene_node is None: continue nodes.append(scene_node) diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index 558b36576f..37345b16b0 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -58,6 +58,8 @@ catalog = i18nCatalog("cura") MODEL_PATH = "3D/3dmodel.model" PACKAGE_METADATA_PATH = "Cura/packages.json" +TEXTURES_PATH = "3D/Textures" +MODEL_RELATIONS_PATH = "3D/_rels/3dmodel.model.rels" class ThreeMFWriter(MeshWriter): def __init__(self): @@ -109,7 +111,11 @@ class ThreeMFWriter(MeshWriter): def _convertUMNodeToSavitarNode(um_node, transformation = Matrix(), exported_settings: Optional[Dict[str, Set[str]]] = None, - center_mesh = False): + center_mesh = False, + scene: Savitar.Scene = None, + archive: zipfile.ZipFile = None, + model_relations_element: ET.Element = None, + content_types_element: ET.Element = None): """Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode :returns: Uranium Scene node. @@ -150,7 +156,28 @@ class ThreeMFWriter(MeshWriter): if indices_array is not None: savitar_node.getMeshData().setFacesFromBytes(indices_array) else: - savitar_node.getMeshData().setFacesFromBytes(numpy.arange(mesh_data.getVertices().size / 3, dtype=numpy.int32).tostring()) + savitar_node.getMeshData().setFacesFromBytes(numpy.arange(mesh_data.getVertices().size / 3, dtype=numpy.int32).tobytes()) + + packed_texture = um_node.callDecoration("packTexture") + uv_coordinates_array = mesh_data.getUVCoordinatesAsByteArray() + if packed_texture is not None and archive is not None and uv_coordinates_array is not None and len(uv_coordinates_array) > 0: + texture_path = f"{TEXTURES_PATH}/{id(um_node)}.png" + texture_file = zipfile.ZipInfo(texture_path) + # Don't try to compress texture file, because the PNG is pretty much as compact as it will get + archive.writestr(texture_file, packed_texture) + + savitar_node.getMeshData().setUVCoordinatesPerVertexAsBytes(uv_coordinates_array, texture_path, scene) + + # Add texture relation to model relations file + if model_relations_element is not None: + ET.SubElement(model_relations_element, "Relationship", + Target=texture_path, Id=f"rel{len(model_relations_element)+1}", + Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dtexture") + + if content_types_element is not None: + ET.SubElement(content_types_element, "Override", PartName=texture_path, + ContentType="application/vnd.ms-package.3dmanufacturing-3dmodeltexture") + # Handle per object settings (if any) stack = um_node.callDecoration("getStack") @@ -187,7 +214,11 @@ class ThreeMFWriter(MeshWriter): if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr: continue savitar_child_node = ThreeMFWriter._convertUMNodeToSavitarNode(child_node, - exported_settings = exported_settings) + exported_settings = exported_settings, + scene = scene, + archive = archive, + model_relations_element = model_relations_element, + content_types_element = content_types_element) if savitar_child_node is not None: savitar_node.addChild(savitar_child_node) @@ -249,6 +280,9 @@ class ThreeMFWriter(MeshWriter): # Create Metadata/_rels/model_settings.config.rels metadata_relations_element = self._makeRelationsTree() + # Create model relations + model_relations_element = self._makeRelationsTree() + # Let the variant add its specific files variant.add_extra_files(archive, metadata_relations_element) @@ -320,13 +354,21 @@ class ThreeMFWriter(MeshWriter): savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(root_child, transformation_matrix, exported_model_settings, - center_mesh = True) + center_mesh = True, + scene = savitar_scene, + archive = archive, + model_relations_element = model_relations_element, + content_types_element = content_types) if savitar_node: savitar_scene.addSceneNode(savitar_node) else: savitar_node = self._convertUMNodeToSavitarNode(node, transformation_matrix, - exported_model_settings) + exported_model_settings, + scene = savitar_scene, + archive = archive, + model_relations_element = model_relations_element, + content_types_element = content_types) if savitar_node: savitar_scene.addSceneNode(savitar_node) @@ -338,6 +380,8 @@ class ThreeMFWriter(MeshWriter): self._storeElementTree(archive, "_rels/.rels", relations_element) if len(metadata_relations_element) > 0: self._storeElementTree(archive, "Metadata/_rels/model_settings.config.rels", metadata_relations_element) + if len(model_relations_element) > 0: + self._storeElementTree(archive, MODEL_RELATIONS_PATH, model_relations_element) except Exception as error: Logger.logException("e", "Error writing zip file") self.setInformation(str(error)) @@ -500,7 +544,7 @@ class ThreeMFWriter(MeshWriter): def sceneNodesToString(scene_nodes: [SceneNode]) -> str: savitar_scene = Savitar.Scene() for scene_node in scene_nodes: - savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(scene_node, center_mesh = True) + savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(scene_node, center_mesh = True, scene = savitar_scene) savitar_scene.addSceneNode(savitar_node) parser = Savitar.ThreeMFParser() scene_string = parser.sceneToString(savitar_scene) diff --git a/plugins/CuraEngineBackend/Cura.proto b/plugins/CuraEngineBackend/Cura.proto index 238829ba64..1636c56c20 100644 --- a/plugins/CuraEngineBackend/Cura.proto +++ b/plugins/CuraEngineBackend/Cura.proto @@ -53,6 +53,8 @@ message Object bytes indices = 4; //An array of ints. repeated Setting settings = 5; // Setting override per object, overruling the global settings. string name = 6; //Mesh name + bytes uv_coordinates = 7; //An array of 2 floats. + bytes texture = 8; //PNG-encoded texture data } message Progress @@ -78,10 +80,14 @@ message Polygon { SkirtType = 5; InfillType = 6; SupportInfillType = 7; - MoveCombingType = 8; - MoveRetractionType = 9; + MoveUnretracted = 8; + MoveRetracted = 9; SupportInterfaceType = 10; PrimeTowerType = 11; + MoveWhileRetracting = 12; + MoveWhileUnretracting = 13; + StationaryRetractUnretract = 14; + NumPrintFeatureTypes = 15; } Type type = 1; // Type of move bytes points = 2; // The points of the polygon, or two points if only a line segment (Currently only line segments are used) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index e3e15f5381..f8736d69b8 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -2,6 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. import argparse #To run the engine in debug mode if the front-end is in debug mode. +from cmath import isnan from collections import defaultdict import os from PyQt6.QtCore import QObject, QTimer, QUrl, pyqtSlot @@ -158,6 +159,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 # Count number of objects to see if there is something changed self._last_num_objects: Dict[int, int] = defaultdict(int) @@ -960,12 +962,44 @@ class CuraEngineBackend(QObject, Backend): """ material_amounts = [] + self._unused_extruders = [] for index in range(message.repeatedMessageCount("materialEstimates")): - material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount) + material_use_for_tool = message.getRepeatedMessage("materialEstimates", index).material_amount + if isnan(material_use_for_tool): + material_amounts.append(0.0) + if self._global_container_stack.extruderList[int(index)].isEnabled: + self._unused_extruders.append(index) + else: + material_amounts.append(material_use_for_tool) + + if self._unused_extruders: + extruder_names = [self._global_container_stack.extruderList[int(idx)].definition.getName() for idx in self._unused_extruders] + unused_extruders = [f"
  • {extruder_name}
  • " for extruder_name in extruder_names] + warning_message = Message( + text=catalog.i18nc("@message", "At least one extruder remains unused in this print:" + f"
    This can sometimes become a problem, " + "for example when the bed temperature is adjusted for the material present in the unused extruder. " + "It might be desirable to disable these unused extruders."), + title=catalog.i18nc("@message:title", "Unused Extruder(s)"), + message_type=Message.MessageType.WARNING + ) + warning_message.addAction("disable_extruders", + name=catalog.i18nc("@button", "Disable unused extruder(s)"), + icon="", + description=catalog.i18nc("@label", "Automatically disable the unused extruder(s)") + ) + warning_message.actionTriggered.connect(self._onMessageActionTriggered) + warning_message.show() times = self._parseMessagePrintTimes(message) self.printDurationMessage.emit(self._start_slice_job_build_plate, times, material_amounts) + def _onMessageActionTriggered(self, message: Message, message_action: str) -> None: + if message_action == "disable_extruders": + message.hide() + for unused_extruder in self._unused_extruders: + CuraApplication.getInstance().getMachineManager().setExtruderEnabled(unused_extruder, False) + def _parseMessagePrintTimes(self, message: Arcus.PythonMessage) -> Dict[str, float]: """Called for parsing message to retrieve estimated time per feature diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index b276469d09..8b27a0319a 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -509,6 +509,14 @@ class StartSliceJob(Job): obj.vertices = flat_verts + uv_coordinates = mesh_data.getUVCoordinates() + if uv_coordinates is not None: + obj.uv_coordinates = uv_coordinates.flatten() + + packed_texture = object.callDecoration("packTexture") + if packed_texture is not None: + obj.texture = packed_texture + self._handlePerObjectSettings(cast(CuraSceneNode, object), obj) Job.yieldThread() diff --git a/plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml b/plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml index ca836ee21d..931a4fe9f0 100644 --- a/plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml +++ b/plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml @@ -11,10 +11,10 @@ Cura.RoundedRectangle width: parent.width height: projectImage.height + 2 * UM.Theme.getSize("default_margin").width cornerSide: Cura.RoundedRectangle.Direction.All - border.color: UM.Theme.getColor("lining") + border.color: enabled ? UM.Theme.getColor("lining") : UM.Theme.getColor("action_button_disabled_border") border.width: UM.Theme.getSize("default_lining").width radius: UM.Theme.getSize("default_radius").width - color: UM.Theme.getColor("main_background") + color: getBackgroundColor() signal clicked() property alias imageSource: projectImage.source property alias projectNameText: displayNameLabel.text @@ -22,17 +22,18 @@ Cura.RoundedRectangle property alias projectLastUpdatedText: lastUpdatedLabel.text property alias cardMouseAreaEnabled: cardMouseArea.enabled - onVisibleChanged: color = UM.Theme.getColor("main_background") + onVisibleChanged: color = getBackgroundColor() MouseArea { id: cardMouseArea anchors.fill: parent - hoverEnabled: true - onEntered: base.color = UM.Theme.getColor("action_button_hovered") - onExited: base.color = UM.Theme.getColor("main_background") + hoverEnabled: base.enabled + onEntered: color = getBackgroundColor() + onExited: color = getBackgroundColor() onClicked: base.clicked() } + Row { id: projectInformationRow @@ -73,7 +74,7 @@ Cura.RoundedRectangle width: parent.width height: Math.round(parent.height / 3) elide: Text.ElideRight - color: UM.Theme.getColor("small_button_text") + color: base.enabled ? UM.Theme.getColor("small_button_text") : UM.Theme.getColor("text_disabled") } UM.Label @@ -82,8 +83,27 @@ Cura.RoundedRectangle width: parent.width height: Math.round(parent.height / 3) elide: Text.ElideRight - color: UM.Theme.getColor("small_button_text") + color: base.enabled ? UM.Theme.getColor("small_button_text") : UM.Theme.getColor("text_disabled") } } } -} \ No newline at end of file + + function getBackgroundColor() + { + if(enabled) + { + if(cardMouseArea.containsMouse) + { + return UM.Theme.getColor("action_button_hovered") + } + else + { + return UM.Theme.getColor("main_background") + } + } + else + { + return UM.Theme.getColor("action_button_disabled") + } + } +} diff --git a/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml b/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml index faceb4df23..2d0bd30f2b 100644 --- a/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml +++ b/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml @@ -159,17 +159,30 @@ Item Repeater { model: manager.digitalFactoryProjectModel - delegate: ProjectSummaryCard + delegate: Item { - id: projectSummaryCard - imageSource: model.thumbnailUrl || "../images/placeholder.svg" - projectNameText: model.displayName - projectUsernameText: model.username - projectLastUpdatedText: "Last updated: " + model.lastUpdated + width: parent.width + height: projectSummaryCard.height - onClicked: + UM.TooltipArea { - manager.selectedProjectIndex = index + anchors.fill: parent + text: "This project is inactive and cannot be used." + enabled: !model.active + } + + ProjectSummaryCard + { + id: projectSummaryCard + imageSource: model.thumbnailUrl || "../images/placeholder.svg" + projectNameText: model.displayName + projectUsernameText: model.username + projectLastUpdatedText: "Last updated: " + model.lastUpdated + enabled: model.active + + onClicked: { + manager.selectedProjectIndex = index + } } } } diff --git a/plugins/DigitalLibrary/src/DigitalFactoryProjectModel.py b/plugins/DigitalLibrary/src/DigitalFactoryProjectModel.py index bd12a4ca12..7140657508 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryProjectModel.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryProjectModel.py @@ -17,6 +17,7 @@ class DigitalFactoryProjectModel(ListModel): ThumbnailUrlRole = Qt.ItemDataRole.UserRole + 5 UsernameRole = Qt.ItemDataRole.UserRole + 6 LastUpdatedRole = Qt.ItemDataRole.UserRole + 7 + ActiveRole = Qt.ItemDataRole.UserRole + 8 dfProjectModelChanged = pyqtSignal() @@ -28,6 +29,7 @@ class DigitalFactoryProjectModel(ListModel): self.addRoleName(self.ThumbnailUrlRole, "thumbnailUrl") self.addRoleName(self.UsernameRole, "username") self.addRoleName(self.LastUpdatedRole, "lastUpdated") + self.addRoleName(self.ActiveRole, "active") self._projects = [] # type: List[DigitalFactoryProjectResponse] def setProjects(self, df_projects: List[DigitalFactoryProjectResponse]) -> None: @@ -59,5 +61,6 @@ class DigitalFactoryProjectModel(ListModel): "thumbnailUrl": project.thumbnail_url, "username": project.username, "lastUpdated": project.last_updated.strftime(PROJECT_UPDATED_AT_DATETIME_FORMAT) if project.last_updated else "", + "active": project.active, }) self.dfProjectModelChanged.emit() diff --git a/plugins/DigitalLibrary/src/DigitalFactoryProjectResponse.py b/plugins/DigitalLibrary/src/DigitalFactoryProjectResponse.py index bef90e5125..303271f211 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryProjectResponse.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryProjectResponse.py @@ -28,6 +28,7 @@ class DigitalFactoryProjectResponse(BaseModel): team_ids: Optional[List[str]] = None, status: Optional[str] = None, technical_requirements: Optional[Dict[str, Any]] = None, + is_inactive: bool = False, **kwargs) -> None: """ Creates a new digital factory project response object @@ -56,6 +57,7 @@ class DigitalFactoryProjectResponse(BaseModel): self.last_updated = datetime.strptime(last_updated, DIGITAL_FACTORY_RESPONSE_DATETIME_FORMAT) if last_updated else None self.status = status self.technical_requirements = technical_requirements + self.active = not is_inactive super().__init__(**kwargs) def __str__(self) -> str: diff --git a/plugins/GCodeReader/FlavorParser.py b/plugins/GCodeReader/FlavorParser.py index 74dbeadec0..f83a9bbb34 100644 --- a/plugins/GCodeReader/FlavorParser.py +++ b/plugins/GCodeReader/FlavorParser.py @@ -133,7 +133,10 @@ class FlavorParser: if i > 0: line_feedrates[i - 1] = point[3] line_types[i - 1] = point[5] - if point[5] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]: + if point[5] in [LayerPolygon.MoveUnretractedType, + LayerPolygon.MoveRetractedType, + LayerPolygon.MoveWhileRetractingType, + LayerPolygon.MoveWhileUnretractingType]: line_widths[i - 1] = 0.1 line_thicknesses[i - 1] = 0.0 # Travels are set as zero thickness lines else: @@ -196,7 +199,7 @@ class FlavorParser: path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], self._layer_type]) # extrusion self._previous_extrusion_value = new_extrusion_value else: - path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) # retraction + path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractedType]) # retraction e[self._extruder_number] = new_extrusion_value # Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions @@ -205,9 +208,9 @@ class FlavorParser: self._current_layer_thickness = z - self._previous_z # allow a tiny overlap self._previous_z = z elif self._previous_extrusion_value > e[self._extruder_number]: - path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) + path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractedType]) else: - path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType]) + path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveUnretractedType]) return self._position(x, y, z, f, e) @@ -419,7 +422,7 @@ class FlavorParser: self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])) current_path.clear() # Start the new layer at the end position of the last layer - current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType]) + current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveUnretractedType]) # When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior # as in ProcessSlicedLayersJob @@ -461,9 +464,9 @@ class FlavorParser: # When changing tool, store the end point of the previous path, then process the code and finally # add another point with the new position of the head. - current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType]) + current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveUnretractedType]) current_position = self.processTCode(global_stack, T, line, current_position, current_path) - current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType]) + current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveUnretractedType]) if line.startswith("M"): M = self._getInt(line, "M") diff --git a/plugins/Marketplace/Marketplace.py b/plugins/Marketplace/Marketplace.py index 86910f8f4a..fc287b5877 100644 --- a/plugins/Marketplace/Marketplace.py +++ b/plugins/Marketplace/Marketplace.py @@ -21,7 +21,6 @@ class Marketplace(Extension, QObject): def __init__(self, parent: Optional[QObject] = None) -> None: QObject.__init__(self, parent) Extension.__init__(self) - self._window: Optional["QObject"] = None # If the window has been loaded yet, it'll be cached in here. self._package_manager = CuraApplication.getInstance().getPackageManager() self._material_package_list: Optional[RemotePackageList] = None @@ -79,20 +78,17 @@ class Marketplace(Extension, QObject): If the window hadn't been loaded yet into Qt, it will be created lazily. """ - if self._window is None: - plugin_registry = PluginRegistry.getInstance() - plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.checkIfRestartNeeded) - plugin_path = plugin_registry.getPluginPath(self.getPluginId()) - if plugin_path is None: - plugin_path = os.path.dirname(__file__) - path = os.path.join(plugin_path, "resources", "qml", "Marketplace.qml") - self._window = CuraApplication.getInstance().createQmlComponent(path, {"manager": self}) - if self._window is None: # Still None? Failed to load the QML then. - return - if not self._window.isVisible(): - self.setTabShown(0) - self._window.show() - self._window.requestActivate() # Bring window into focus, if it was already open in the background. + + plugin_registry = PluginRegistry.getInstance() + plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.checkIfRestartNeeded) + plugin_path = plugin_registry.getPluginPath(self.getPluginId()) + if plugin_path is None: + plugin_path = os.path.dirname(__file__) + path = os.path.join(plugin_path, "resources", "qml", "Marketplace.qml") + window = CuraApplication.getInstance().createQmlSubWindow(path, {"manager": self}) + + if window is not None: # Still None? Failed to load the QML then. + window.show() @pyqtSlot() def setVisibleTabToMaterials(self) -> None: @@ -103,9 +99,6 @@ class Marketplace(Extension, QObject): self.setTabShown(1) def checkIfRestartNeeded(self) -> None: - if self._window is None: - return - if self._package_manager.hasPackagesToRemoveOrInstall or \ PluginRegistry.getInstance().getCurrentSessionActivationChangedPlugins(): self._restart_needed = True diff --git a/plugins/Marketplace/resources/qml/Marketplace.qml b/plugins/Marketplace/resources/qml/Marketplace.qml index 8028b89e02..c858297ac9 100644 --- a/plugins/Marketplace/resources/qml/Marketplace.qml +++ b/plugins/Marketplace/resources/qml/Marketplace.qml @@ -9,7 +9,7 @@ import QtQuick.Window 2.2 import UM 1.5 as UM import Cura 1.6 as Cura -Window +UM.Dialog { id: marketplaceDialog property variant catalog: UM.I18nCatalog { name: "cura" } @@ -25,293 +25,289 @@ Window width: minimumWidth height: minimumHeight - onVisibleChanged: - { - while(contextStack.depth > 1) - { - contextStack.pop(); //Do NOT use the StackView.Immediate transition here, since it causes the window to stay empty. Seemingly a Qt bug: https://bugreports.qt.io/browse/QTBUG-60670? - } - } - - Connections - { - target: Cura.API.account - function onLoginStateChanged() - { - close(); - } - } - title: "Marketplace" //Seen by Ultimaker as a brand name, so this doesn't get translated. - modality: Qt.NonModal // Background color Rectangle { anchors.fill: parent color: UM.Theme.getColor("main_background") - } - //The Marketplace can have a page in front of everything with package details. The stack view controls its visibility. - StackView - { - id: contextStack - anchors.fill: parent - initialItem: packageBrowse - - ColumnLayout + //The Marketplace can have a page in front of everything with package details. The stack view controls its visibility. + StackView { - id: packageBrowse + id: contextStack + anchors.fill: parent - spacing: UM.Theme.getSize("narrow_margin").height + initialItem: packageBrowse - // Page title. - Item + ColumnLayout { - Layout.preferredWidth: parent.width - Layout.preferredHeight: childrenRect.height + UM.Theme.getSize("default_margin").height + id: packageBrowse - UM.Label + spacing: UM.Theme.getSize("narrow_margin").height + + // Page title. + Item { - id: pageTitle - anchors - { - left: parent.left - leftMargin: UM.Theme.getSize("default_margin").width - right: parent.right - rightMargin: UM.Theme.getSize("default_margin").width - bottom: parent.bottom - } + Layout.preferredWidth: parent.width + Layout.preferredHeight: childrenRect.height + UM.Theme.getSize("default_margin").height - font: UM.Theme.getFont("large") - text: content.item ? content.item.pageTitle: catalog.i18nc("@title", "Loading...") + UM.Label + { + id: pageTitle + anchors + { + left: parent.left + leftMargin: UM.Theme.getSize("default_margin").width + right: parent.right + rightMargin: UM.Theme.getSize("default_margin").width + bottom: parent.bottom + } + + font: UM.Theme.getFont("large") + text: content.item ? content.item.pageTitle : catalog.i18nc("@title", "Loading...") + } } - } - OnboardBanner - { - id: onBoardBanner - visible: content.item && content.item.bannerVisible - text: content.item && content.item.bannerText - icon: content.item && content.item.bannerIcon - onRemove: content.item && content.item.onRemoveBanner - readMoreUrl: content.item && content.item.bannerReadMoreUrl - - Layout.fillWidth: true - Layout.leftMargin: UM.Theme.getSize("default_margin").width - Layout.rightMargin: UM.Theme.getSize("default_margin").width - } - - // Search & Top-Level Tabs - Item - { - id: searchHeader - implicitHeight: childrenRect.height - implicitWidth: parent.width - 2 * UM.Theme.getSize("default_margin").width - Layout.alignment: Qt.AlignHCenter - RowLayout + OnboardBanner { - width: parent.width - height: UM.Theme.getSize("button_icon").height + UM.Theme.getSize("default_margin").height - spacing: UM.Theme.getSize("thin_margin").width + id: onBoardBanner + visible: content.item && content.item.bannerVisible + text: content.item && content.item.bannerText + icon: content.item && content.item.bannerIcon + onRemove: content.item && content.item.onRemoveBanner + readMoreUrl: content.item && content.item.bannerReadMoreUrl - Cura.SearchBar + Layout.fillWidth: true + Layout.leftMargin: UM.Theme.getSize("default_margin").width + Layout.rightMargin: UM.Theme.getSize("default_margin").width + } + + // Search & Top-Level Tabs + Item + { + id: searchHeader + implicitHeight: childrenRect.height + implicitWidth: parent.width - 2 * UM.Theme.getSize("default_margin").width + Layout.alignment: Qt.AlignHCenter + RowLayout { - id: searchBar - implicitHeight: UM.Theme.getSize("button_icon").height - Layout.fillWidth: true - onTextEdited: searchStringChanged(text) - } + width: parent.width + height: UM.Theme.getSize("button_icon").height + UM.Theme.getSize("default_margin").height + spacing: UM.Theme.getSize("thin_margin").width - // Page selection. - TabBar - { - id: pageSelectionTabBar - Layout.alignment: Qt.AlignRight - height: UM.Theme.getSize("button_icon").height - spacing: 0 - background: Rectangle { color: "transparent" } - currentIndex: manager.tabShown - - onCurrentIndexChanged: + Cura.SearchBar { - manager.tabShown = currentIndex - searchBar.text = ""; - searchBar.visible = currentItem.hasSearch; - content.source = currentItem.sourcePage; + id: searchBar + implicitHeight: UM.Theme.getSize("button_icon").height + Layout.fillWidth: true + onTextEdited: searchStringChanged(text) } - PackageTypeTab + // Page selection. + TabBar { - id: pluginTabText - width: implicitWidth - text: catalog.i18nc("@button", "Plugins") - property string sourcePage: "Plugins.qml" - property bool hasSearch: true - } - PackageTypeTab - { - id: materialsTabText - width: implicitWidth - text: catalog.i18nc("@button", "Materials") - property string sourcePage: "Materials.qml" - property bool hasSearch: true - } - ManagePackagesButton - { - property string sourcePage: "ManagedPackages.qml" - property bool hasSearch: false + id: pageSelectionTabBar + Layout.alignment: Qt.AlignRight + height: UM.Theme.getSize("button_icon").height + spacing: 0 + background: Rectangle { + color: "transparent" + } + currentIndex: manager.tabShown - Cura.NotificationIcon + onCurrentIndexChanged: { - anchors - { - horizontalCenter: parent.right - verticalCenter: parent.top - } - visible: CuraApplication.getPackageManager().packagesWithUpdate.length > 0 + manager.tabShown = currentIndex + searchBar.text = ""; + searchBar.visible = currentItem.hasSearch; + content.source = currentItem.sourcePage; + } - labelText: + PackageTypeTab + { + id: pluginTabText + width: implicitWidth + text: catalog.i18nc("@button", "Plugins") + property string sourcePage: "Plugins.qml" + property bool hasSearch: true + } + PackageTypeTab + { + id: materialsTabText + width: implicitWidth + text: catalog.i18nc("@button", "Materials") + property string sourcePage: "Materials.qml" + property bool hasSearch: true + } + ManagePackagesButton + { + property string sourcePage: "ManagedPackages.qml" + property bool hasSearch: false + + Cura.NotificationIcon { - const itemCount = CuraApplication.getPackageManager().packagesWithUpdate.length - return itemCount > 9 ? "9+" : itemCount + anchors + { + horizontalCenter: parent.right + verticalCenter: parent.top + } + visible: CuraApplication.getPackageManager().packagesWithUpdate.length > 0 + + labelText: + { + const itemCount = CuraApplication.getPackageManager().packagesWithUpdate.length + return itemCount > 9 ? "9+" : itemCount + } } } } } } - } - FontMetrics - { - id: fontMetrics - font: UM.Theme.getFont("default") - } + FontMetrics + { + id: fontMetrics + font: UM.Theme.getFont("default") + } - Cura.TertiaryButton - { - text: catalog.i18nc("@info", "Search in the browser") - iconSource: UM.Theme.getIcon("LinkExternal") - visible: pageSelectionTabBar.currentItem.hasSearch && searchHeader.visible - isIconOnRightSide: true - height: fontMetrics.height - textFont: fontMetrics.font - textColor: UM.Theme.getColor("text") + Cura.TertiaryButton + { + text: catalog.i18nc("@info", "Search in the browser") + iconSource: UM.Theme.getIcon("LinkExternal") + visible: pageSelectionTabBar.currentItem.hasSearch && searchHeader.visible + isIconOnRightSide: true + height: fontMetrics.height + textFont: fontMetrics.font + textColor: UM.Theme.getColor("text") - onClicked: content.item && Qt.openUrlExternally(content.item.searchInBrowserUrl) - } - - // Page contents. - Rectangle - { - Layout.preferredWidth: parent.width - Layout.fillHeight: true - color: UM.Theme.getColor("detail_background") + onClicked: content.item && Qt.openUrlExternally(content.item.searchInBrowserUrl) + } // Page contents. - Loader + Rectangle { - id: content - anchors.fill: parent - anchors.margins: UM.Theme.getSize("default_margin").width - source: "Plugins.qml" + Layout.preferredWidth: parent.width + Layout.fillHeight: true + color: UM.Theme.getColor("detail_background") - Connections + // Page contents. + Loader { - target: content - function onLoaded() + id: content + anchors.fill: parent + anchors.margins: UM.Theme.getSize("default_margin").width + source: "Plugins.qml" + + Connections { - pageTitle.text = content.item.pageTitle - searchStringChanged.connect(handleSearchStringChanged) - } - function handleSearchStringChanged(new_search) - { - content.item.model.searchString = new_search + target: content + + function onLoaded() + { + pageTitle.text = content.item.pageTitle + searchStringChanged.connect(handleSearchStringChanged) + } + + function handleSearchStringChanged(new_search) + { + content.item.model.searchString = new_search + } } } } } } - } - Rectangle - { - height: quitButton.height + 2 * UM.Theme.getSize("default_margin").width - color: UM.Theme.getColor("primary") - visible: manager.showRestartNotification - anchors - { - left: parent.left - right: parent.right - bottom: parent.bottom - } - - RowLayout + Rectangle { + height: quitButton.height + 2 * UM.Theme.getSize("default_margin").width + color: UM.Theme.getColor("primary") + visible: manager.showRestartNotification anchors { left: parent.left right: parent.right - verticalCenter: parent.verticalCenter - margins: UM.Theme.getSize("default_margin").width + bottom: parent.bottom } - spacing: UM.Theme.getSize("default_margin").width - UM.ColorImage - { - id: bannerIcon - source: UM.Theme.getIcon("Plugin") - color: UM.Theme.getColor("primary_button_text") - implicitWidth: UM.Theme.getSize("banner_icon_size").width - implicitHeight: UM.Theme.getSize("banner_icon_size").height - } - Text + RowLayout { - color: UM.Theme.getColor("primary_button_text") - text: catalog.i18nc("@button", "In order to use the package you will need to restart Cura") - font: UM.Theme.getFont("default") - renderType: Text.NativeRendering - Layout.fillWidth: true - } - Cura.SecondaryButton - { - id: quitButton - text: catalog.i18nc("@info:button, %1 is the application name", "Quit %1").arg(CuraApplication.applicationDisplayName) - onClicked: + anchors { - marketplaceDialog.hide(); - CuraApplication.checkAndExitApplication(); + left: parent.left + right: parent.right + verticalCenter: parent.verticalCenter + margins: UM.Theme.getSize("default_margin").width + } + spacing: UM.Theme.getSize("default_margin").width + UM.ColorImage + { + id: bannerIcon + source: UM.Theme.getIcon("Plugin") + + color: UM.Theme.getColor("primary_button_text") + implicitWidth: UM.Theme.getSize("banner_icon_size").width + implicitHeight: UM.Theme.getSize("banner_icon_size").height + } + Text + { + color: UM.Theme.getColor("primary_button_text") + text: catalog.i18nc("@button", "In order to use the package you will need to restart Cura") + font: UM.Theme.getFont("default") + renderType: Text.NativeRendering + Layout.fillWidth: true + } + Cura.SecondaryButton + { + id: quitButton + text: catalog.i18nc("@info:button, %1 is the application name", "Quit %1").arg(CuraApplication.applicationDisplayName) + onClicked: + { + marketplaceDialog.hide(); + CuraApplication.checkAndExitApplication(); + } } } } - } - Rectangle - { - color: UM.Theme.getColor("main_background") - anchors.fill: parent - visible: !Cura.API.account.isLoggedIn && CuraApplication.isEnterprise - - UM.Label + Rectangle { - id: signInLabel - anchors.centerIn: parent - width: Math.round(UM.Theme.getSize("modal_window_minimum").width / 2.5) - text: catalog.i18nc("@description","Please sign in to get verified plugins and materials for UltiMaker Cura Enterprise") - horizontalAlignment: Text.AlignHCenter + color: UM.Theme.getColor("main_background") + anchors.fill: parent + visible: !Cura.API.account.isLoggedIn && CuraApplication.isEnterprise + + UM.Label + { + id: signInLabel + anchors.centerIn: parent + width: Math.round(UM.Theme.getSize("modal_window_minimum").width / 2.5) + text: catalog.i18nc("@description", "Please sign in to get verified plugins and materials for UltiMaker Cura Enterprise") + horizontalAlignment: Text.AlignHCenter + } + + Cura.PrimaryButton + { + id: loginButton + width: UM.Theme.getSize("account_button").width + height: UM.Theme.getSize("account_button").height + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: signInLabel.bottom + anchors.topMargin: UM.Theme.getSize("default_margin").height * 2 + text: catalog.i18nc("@button", "Sign in") + fixedWidthMode: true + onClicked: Cura.API.account.login() + } } - Cura.PrimaryButton + Connections { - id: loginButton - width: UM.Theme.getSize("account_button").width - height: UM.Theme.getSize("account_button").height - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: signInLabel.bottom - anchors.topMargin: UM.Theme.getSize("default_margin").height * 2 - text: catalog.i18nc("@button", "Sign in") - fixedWidthMode: true - onClicked: Cura.API.account.login() + target: Cura.API.account + function onLoginStateChanged() + { + reject(); + } } } } diff --git a/plugins/PaintTool/BrushColorButton.qml b/plugins/PaintTool/BrushColorButton.qml new file mode 100644 index 0000000000..b62ab09e92 --- /dev/null +++ b/plugins/PaintTool/BrushColorButton.qml @@ -0,0 +1,18 @@ +// Copyright (c) 2025 UltiMaker +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick + +import UM 1.7 as UM +import Cura 1.0 as Cura + + +UM.ToolbarButton +{ + id: buttonBrushColor + + property string color + + checked: UM.Controller.properties.getValue("BrushColor") === buttonBrushColor.color + onClicked: UM.Controller.setProperty("BrushColor", buttonBrushColor.color) +} diff --git a/plugins/PaintTool/BrushShapeButton.qml b/plugins/PaintTool/BrushShapeButton.qml new file mode 100644 index 0000000000..e05cd206f3 --- /dev/null +++ b/plugins/PaintTool/BrushShapeButton.qml @@ -0,0 +1,18 @@ +// Copyright (c) 2025 UltiMaker +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick + +import UM 1.7 as UM +import Cura 1.0 as Cura + + +UM.ToolbarButton +{ + id: buttonBrushShape + + property int shape + + checked: UM.Controller.properties.getValue("BrushShape") === buttonBrushShape.shape + onClicked: UM.Controller.setProperty("BrushShape", buttonBrushShape.shape) +} diff --git a/plugins/PaintTool/PaintModeButton.qml b/plugins/PaintTool/PaintModeButton.qml new file mode 100644 index 0000000000..833a009551 --- /dev/null +++ b/plugins/PaintTool/PaintModeButton.qml @@ -0,0 +1,18 @@ +// Copyright (c) 2025 UltiMaker +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick + +import UM 1.7 as UM +import Cura 1.0 as Cura + + +Cura.ModeSelectorButton +{ + id: modeSelectorButton + + property string mode + + selected: UM.Controller.properties.getValue("PaintType") === modeSelectorButton.mode + onClicked: UM.Controller.setProperty("PaintType", modeSelectorButton.mode) +} diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py new file mode 100644 index 0000000000..e67795301d --- /dev/null +++ b/plugins/PaintTool/PaintTool.py @@ -0,0 +1,424 @@ +# Copyright (c) 2025 UltiMaker +# Cura is released under the terms of the LGPLv3 or higher. + +from enum import IntEnum +import numpy +from PyQt6.QtCore import Qt, QObject, pyqtEnum +from PyQt6.QtGui import QImage, QPainter, QColor, QPen +from PyQt6 import QtWidgets +from typing import cast, Dict, List, Optional, Tuple + +from numpy import ndarray + +from UM.Application import Application +from UM.Event import Event, MouseEvent, KeyEvent +from UM.Job import Job +from UM.Logger import Logger +from UM.Scene.SceneNode import SceneNode +from UM.Scene.Selection import Selection +from UM.Tool import Tool +from UM.View.GL.OpenGL import OpenGL + +from cura.CuraApplication import CuraApplication +from cura.PickingPass import PickingPass +from UM.View.SelectionPass import SelectionPass +from .PaintView import PaintView +from .PrepareTextureJob import PrepareTextureJob + + +class PaintTool(Tool): + """Provides the tool to paint meshes.""" + + class Brush(QObject): + @pyqtEnum + class Shape(IntEnum): + SQUARE = 0 + CIRCLE = 1 + + class Paint(QObject): + @pyqtEnum + class State(IntEnum): + MULTIPLE_SELECTION = 0 # Multiple objects are selected, wait until there is only one + PREPARING_MODEL = 1 # Model is being prepared (UV-unwrapping, texture generation) + READY = 2 # Ready to paint ! + + def __init__(self, view: PaintView) -> None: + super().__init__() + + self._view: PaintView = view + self._view.canUndoChanged.connect(self._onCanUndoChanged) + self._view.canRedoChanged.connect(self._onCanRedoChanged) + + self._picking_pass: Optional[PickingPass] = None + self._faces_selection_pass: Optional[SelectionPass] = None + + self._shortcut_key: Qt.Key = Qt.Key.Key_P + + self._node_cache: Optional[SceneNode] = None + self._mesh_transformed_cache = None + self._cache_dirty: bool = True + + self._brush_size: int = 200 + self._brush_color: str = "preferred" + self._brush_shape: PaintTool.Brush.Shape = PaintTool.Brush.Shape.CIRCLE + self._brush_pen: QPen = self._createBrushPen() + + self._mouse_held: bool = False + + self._last_text_coords: Optional[numpy.ndarray] = None + self._last_mouse_coords: Optional[Tuple[int, int]] = None + self._last_face_id: Optional[int] = None + + self._state: PaintTool.Paint.State = PaintTool.Paint.State.MULTIPLE_SELECTION + self._prepare_texture_job: Optional[PrepareTextureJob] = None + + self.setExposedProperties("PaintType", "BrushSize", "BrushColor", "BrushShape", "State", "CanUndo", "CanRedo") + + self._controller.activeViewChanged.connect(self._updateIgnoreUnselectedObjects) + self._controller.activeToolChanged.connect(self._updateState) + + def _createBrushPen(self) -> QPen: + pen = QPen() + pen.setWidth(self._brush_size) + pen.setColor(Qt.GlobalColor.white) + + match self._brush_shape: + case PaintTool.Brush.Shape.SQUARE: + pen.setCapStyle(Qt.PenCapStyle.SquareCap) + case PaintTool.Brush.Shape.CIRCLE: + pen.setCapStyle(Qt.PenCapStyle.RoundCap) + return pen + + def _createStrokeImage(self, x0: float, y0: float, x1: float, y1: float) -> Tuple[QImage, Tuple[int, int]]: + xdiff = int(x1 - x0) + ydiff = int(y1 - y0) + + half_brush_size = self._brush_size // 2 + start_x = int(min(x0, x1) - half_brush_size) + start_y = int(min(y0, y1) - half_brush_size) + + stroke_image = QImage(abs(xdiff) + self._brush_size, abs(ydiff) + self._brush_size, QImage.Format.Format_RGB32) + stroke_image.fill(0) + + painter = QPainter(stroke_image) + painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) + painter.setPen(self._brush_pen) + if xdiff == 0 and ydiff == 0: + painter.drawPoint(int(x0 - start_x), int(y0 - start_y)) + else: + painter.drawLine(int(x0 - start_x), int(y0 - start_y), int(x1 - start_x), int(y1 - start_y)) + painter.end() + + return stroke_image, (start_x, start_y) + + def getPaintType(self) -> str: + return self._view.getPaintType() + + def setPaintType(self, paint_type: str) -> None: + if paint_type != self.getPaintType(): + self._view.setPaintType(paint_type) + + self._brush_pen = self._createBrushPen() + self._updateScene() + self.propertyChanged.emit() + + def getBrushSize(self) -> int: + return self._brush_size + + def setBrushSize(self, brush_size: float) -> None: + brush_size_int = int(brush_size) + if brush_size_int != self._brush_size: + self._brush_size = brush_size_int + self._brush_pen = self._createBrushPen() + self.propertyChanged.emit() + + def getBrushColor(self) -> str: + return self._brush_color + + def setBrushColor(self, brush_color: str) -> None: + if brush_color != self._brush_color: + self._brush_color = brush_color + self.propertyChanged.emit() + + def getBrushShape(self) -> int: + return self._brush_shape + + def setBrushShape(self, brush_shape: int) -> None: + if brush_shape != self._brush_shape: + self._brush_shape = brush_shape + self._brush_pen = self._createBrushPen() + self.propertyChanged.emit() + + def getCanUndo(self) -> bool: + return self._view.canUndo() + + def getState(self) -> int: + return self._state + + def _onCanUndoChanged(self): + self.propertyChanged.emit() + + def getCanRedo(self) -> bool: + return self._view.canRedo() + + def _onCanRedoChanged(self): + self.propertyChanged.emit() + + def undoStackAction(self) -> None: + self._view.undoStroke() + self._updateScene() + + def redoStackAction(self) -> None: + self._view.redoStroke() + self._updateScene() + + def clear(self) -> None: + width, height = self._view.getUvTexDimensions() + clear_image = QImage(width, height, QImage.Format.Format_RGB32) + clear_image.fill(Qt.GlobalColor.white) + self._view.addStroke(clear_image, 0, 0, "none", False) + + self._updateScene() + + @staticmethod + def _get_intersect_ratio_via_pt(a: numpy.ndarray, pt: numpy.ndarray, b: numpy.ndarray, c: numpy.ndarray) -> float: + # compute the intersection of (param) A - pt with (param) B - (param) C + if all(a == pt) or all(b == c) or all(a == c) or all(a == b): + return 1.0 + + # compute unit vectors of directions of lines A and B + udir_a = a - pt + udir_a /= numpy.linalg.norm(udir_a) + udir_b = b - c + udir_b /= numpy.linalg.norm(udir_b) + + # find unit direction vector for line C, which is perpendicular to lines A and B + udir_res = numpy.cross(udir_b, udir_a) + udir_res_len = numpy.linalg.norm(udir_res) + if udir_res_len == 0: + return 1.0 + udir_res /= udir_res_len + + # solve system of equations + rhs = b - a + lhs = numpy.array([udir_a, -udir_b, udir_res]).T + try: + solved = numpy.linalg.solve(lhs, rhs) + except numpy.linalg.LinAlgError: + return 1.0 + + # get the ratio + intersect = ((a + solved[0] * udir_a) + (b + solved[1] * udir_b)) * 0.5 + a_intersect_dist = numpy.linalg.norm(a - intersect) + if a_intersect_dist == 0: + return 1.0 + return numpy.linalg.norm(pt - intersect) / a_intersect_dist + + def _nodeTransformChanged(self, *args) -> None: + self._cache_dirty = True + + def _getTexCoordsFromClick(self, node: SceneNode, x: float, y: float) -> Tuple[int, Optional[numpy.ndarray]]: + face_id = self._faces_selection_pass.getFaceIdAtPosition(x, y) + if face_id < 0 or face_id >= node.getMeshData().getFaceCount(): + return face_id, None + + pt = self._picking_pass.getPickedPosition(x, y).getData() + + va, vb, vc = self._mesh_transformed_cache.getFaceNodes(face_id) + + face_uv_coordinates = node.getMeshData().getFaceUvCoords(face_id) + if face_uv_coordinates is None: + return face_id, None + ta, tb, tc = face_uv_coordinates + + # 'Weight' of each vertex that would produce point pt, so we can generate the texture coordinates from the uv ones of the vertices. + # See (also) https://mathworld.wolfram.com/BarycentricCoordinates.html + wa = PaintTool._get_intersect_ratio_via_pt(va, pt, vb, vc) + wb = PaintTool._get_intersect_ratio_via_pt(vb, pt, vc, va) + wc = PaintTool._get_intersect_ratio_via_pt(vc, pt, va, vb) + wt = wa + wb + wc + if wt == 0: + return face_id, None + wa /= wt + wb /= wt + wc /= wt + texcoords = wa * ta + wb * tb + wc * tc + return face_id, texcoords + + def _iteratateSplitSubstroke(self, node, substrokes, + info_a: Tuple[Tuple[float, float], Tuple[int, Optional[numpy.ndarray]]], + info_b: Tuple[Tuple[float, float], Tuple[int, Optional[numpy.ndarray]]]) -> None: + click_a, (face_a, texcoords_a) = info_a + click_b, (face_b, texcoords_b) = info_b + + if (abs(click_a[0] - click_b[0]) < 0.0001 and abs(click_a[1] - click_b[1]) < 0.0001) or (face_a < 0 and face_b < 0): + return + if face_b < 0 or face_a == face_b: + substrokes.append((self._last_text_coords, texcoords_a)) + return + if face_a < 0: + substrokes.append((self._last_text_coords, texcoords_b)) + return + + mouse_mid = (click_a[0] + click_b[0]) / 2.0, (click_a[1] + click_b[1]) / 2.0 + face_mid, texcoords_mid = self._getTexCoordsFromClick(node, mouse_mid[0], mouse_mid[1]) + mid_struct = (mouse_mid, (face_mid, texcoords_mid)) + if face_mid == face_a: + substrokes.append((texcoords_a, texcoords_mid)) + self._iteratateSplitSubstroke(node, substrokes, mid_struct, info_b) + elif face_mid == face_b: + substrokes.append((texcoords_mid, texcoords_b)) + self._iteratateSplitSubstroke(node, substrokes, info_a, mid_struct) + else: + self._iteratateSplitSubstroke(node, substrokes, mid_struct, info_b) + self._iteratateSplitSubstroke(node, substrokes, info_a, mid_struct) + + def event(self, event: Event) -> bool: + """Handle mouse and keyboard events. + + :param event: The event to handle. + :return: Whether this event has been caught by this tool (True) or should + be passed on (False). + """ + super().event(event) + + controller = Application.getInstance().getController() + node = Selection.getSelectedObject(0) + if node is None: + return False + + # Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes + if event.type == Event.ToolActivateEvent: + return True + + if event.type == Event.ToolDeactivateEvent: + return True + + if self._state != PaintTool.Paint.State.READY: + return False + + if event.type == Event.MouseReleaseEvent and self._controller.getToolsEnabled(): + if MouseEvent.LeftButton not in cast(MouseEvent, event).buttons: + return False + self._mouse_held = False + self._last_text_coords = None + self._last_mouse_coords = None + self._last_face_id = None + return True + + is_moved = event.type == Event.MouseMoveEvent + is_pressed = event.type == Event.MousePressEvent + if (is_moved or is_pressed) and self._controller.getToolsEnabled(): + if is_moved and not self._mouse_held: + return False + + mouse_evt = cast(MouseEvent, event) + if is_pressed: + if MouseEvent.LeftButton not in mouse_evt.buttons: + return False + else: + self._mouse_held = True + + if not self._faces_selection_pass: + self._faces_selection_pass = CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces") + if not self._faces_selection_pass: + return False + + if not self._picking_pass: + self._picking_pass = CuraApplication.getInstance().getRenderer().getRenderPass("picking_selected") + if not self._picking_pass: + return False + + camera = self._controller.getScene().getActiveCamera() + if not camera: + return False + + if node != self._node_cache: + if self._node_cache is not None: + self._node_cache.transformationChanged.disconnect(self._nodeTransformChanged) + self._node_cache = node + self._node_cache.transformationChanged.connect(self._nodeTransformChanged) + self._cache_dirty = True + if self._cache_dirty: + self._cache_dirty = False + self._mesh_transformed_cache = self._node_cache.getMeshDataTransformed() + if not self._mesh_transformed_cache: + return False + + face_id, texcoords = self._getTexCoordsFromClick(node, mouse_evt.x, mouse_evt.y) + if texcoords is None: + return False + if self._last_text_coords is None: + self._last_text_coords = texcoords + self._last_mouse_coords = (mouse_evt.x, mouse_evt.y) + self._last_face_id = face_id + + substrokes = [] + if face_id == self._last_face_id: + substrokes.append((self._last_text_coords, texcoords)) + else: + self._iteratateSplitSubstroke(node, substrokes, + (self._last_mouse_coords, (self._last_face_id, self._last_text_coords)), + ((mouse_evt.x, mouse_evt.y), (face_id, texcoords))) + + w, h = self._view.getUvTexDimensions() + for start_coords, end_coords in substrokes: + sub_image, (start_x, start_y) = self._createStrokeImage( + start_coords[0] * w, + start_coords[1] * h, + end_coords[0] * w, + end_coords[1] * h + ) + self._view.addStroke(sub_image, start_x, start_y, self._brush_color, is_moved) + + self._last_text_coords = texcoords + self._last_mouse_coords = (mouse_evt.x, mouse_evt.y) + self._last_face_id = face_id + self._updateScene(node) + return True + + return False + + def getRequiredExtraRenderingPasses(self) -> list[str]: + return ["selection_faces", "picking_selected"] + + @staticmethod + def _updateScene(node: SceneNode = None): + if node is None: + node = Selection.getSelectedObject(0) + if node is not None: + Application.getInstance().getController().getScene().sceneChanged.emit(node) + + def _onSelectionChanged(self): + super()._onSelectionChanged() + + self.setActiveView("PaintTool" if len(Selection.getAllSelectedObjects()) == 1 else None) + self._updateState() + + def _updateState(self): + if len(Selection.getAllSelectedObjects()) == 1 and self._controller.getActiveTool() == self: + selected_object = Selection.getSelectedObject(0) + if selected_object.callDecoration("getPaintTexture") is not None: + new_state = PaintTool.Paint.State.READY + else: + new_state = PaintTool.Paint.State.PREPARING_MODEL + self._prepare_texture_job = PrepareTextureJob(selected_object) + self._prepare_texture_job.finished.connect(self._onPrepareTextureFinished) + self._prepare_texture_job.start() + else: + new_state = PaintTool.Paint.State.MULTIPLE_SELECTION + + if new_state != self._state: + self._state = new_state + self.propertyChanged.emit() + + def _onPrepareTextureFinished(self, job: Job): + if job == self._prepare_texture_job: + self._prepare_texture_job = None + self._state = PaintTool.Paint.State.READY + self.propertyChanged.emit() + + def _updateIgnoreUnselectedObjects(self): + ignore_unselected_objects = self._controller.getActiveView().name == "PaintTool" + CuraApplication.getInstance().getRenderer().getRenderPass("selection").setIgnoreUnselectedObjects(ignore_unselected_objects) + CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces").setIgnoreUnselectedObjects(ignore_unselected_objects) \ No newline at end of file diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml new file mode 100644 index 0000000000..548b6b047e --- /dev/null +++ b/plugins/PaintTool/PaintTool.qml @@ -0,0 +1,301 @@ +// Copyright (c) 2025 UltiMaker +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import UM 1.7 as UM +import Cura 1.0 as Cura + +Item +{ + id: base + width: childrenRect.width + height: childrenRect.height + UM.I18nCatalog { id: catalog; name: "cura"} + + Action + { + id: undoAction + shortcut: "Ctrl+L" + enabled: UM.Controller.properties.getValue("CanUndo") + onTriggered: UM.Controller.triggerAction("undoStackAction") + } + + Action + { + id: redoAction + shortcut: "Ctrl+Shift+L" + enabled: UM.Controller.properties.getValue("CanRedo") + onTriggered: UM.Controller.triggerAction("redoStackAction") + } + + Column + { + id: mainColumn + spacing: UM.Theme.getSize("default_margin").height + + RowLayout + { + id: rowPaintMode + width: parent.width + + PaintModeButton + { + text: catalog.i18nc("@action:button", "Seam") + icon: "Seam" + tooltipText: catalog.i18nc("@tooltip", "Refine seam placement by defining preferred/avoidance areas") + mode: "seam" + } + + PaintModeButton + { + text: catalog.i18nc("@action:button", "Support") + icon: "Support" + tooltipText: catalog.i18nc("@tooltip", "Refine support placement by defining preferred/avoidance areas") + mode: "support" + visible: false + } + } + + //Line between the sections. + Rectangle + { + width: parent.width + height: UM.Theme.getSize("default_lining").height + color: UM.Theme.getColor("lining") + } + + RowLayout + { + id: rowBrushColor + + UM.Label + { + text: catalog.i18nc("@label", "Mark as") + } + + BrushColorButton + { + id: buttonPreferredArea + color: "preferred" + + text: catalog.i18nc("@action:button", "Preferred") + toolItem: UM.ColorImage + { + source: UM.Theme.getIcon("CheckBadge", "low") + color: UM.Theme.getColor("paint_preferred_area") + } + } + + BrushColorButton + { + id: buttonAvoidArea + color: "avoid" + + text: catalog.i18nc("@action:button", "Avoid") + toolItem: UM.ColorImage + { + source: UM.Theme.getIcon("CancelBadge", "low") + color: UM.Theme.getColor("paint_avoid_area") + } + } + + BrushColorButton + { + id: buttonEraseArea + color: "none" + + text: catalog.i18nc("@action:button", "Erase") + toolItem: UM.ColorImage + { + source: UM.Theme.getIcon("Eraser") + color: UM.Theme.getColor("icon") + } + } + } + + RowLayout + { + id: rowBrushShape + + UM.Label + { + text: catalog.i18nc("@label", "Brush Shape") + } + + BrushShapeButton + { + id: buttonBrushCircle + shape: Cura.PaintToolBrush.CIRCLE + + text: catalog.i18nc("@action:button", "Circle") + toolItem: UM.ColorImage + { + source: UM.Theme.getIcon("Circle") + color: UM.Theme.getColor("icon") + } + } + + BrushShapeButton + { + id: buttonBrushSquare + shape: Cura.PaintToolBrush.SQUARE + + text: catalog.i18nc("@action:button", "Square") + toolItem: UM.ColorImage + { + source: UM.Theme.getIcon("MeshTypeNormal") + color: UM.Theme.getColor("icon") + } + } + } + + UM.Label + { + text: catalog.i18nc("@label", "Brush Size") + } + + UM.Slider + { + id: shapeSizeSlider + width: parent.width + indicatorVisible: false + + from: 10 + to: 1000 + value: UM.Controller.properties.getValue("BrushSize") + + onPressedChanged: function(pressed) + { + if(! pressed) + { + UM.Controller.setProperty("BrushSize", shapeSizeSlider.value); + } + } + } + + //Line between the sections. + Rectangle + { + width: parent.width + height: UM.Theme.getSize("default_lining").height + color: UM.Theme.getColor("lining") + } + + RowLayout + { + UM.ToolbarButton + { + id: undoButton + + enabled: undoAction.enabled + text: catalog.i18nc("@action:button", "Undo Stroke") + toolItem: UM.ColorImage + { + source: UM.Theme.getIcon("ArrowReset") + color: UM.Theme.getColor("icon") + } + + onClicked: undoAction.trigger() + } + + UM.ToolbarButton + { + id: redoButton + + enabled: redoAction.enabled + text: catalog.i18nc("@action:button", "Redo Stroke") + toolItem: UM.ColorImage + { + source: UM.Theme.getIcon("ArrowReset") + color: UM.Theme.getColor("icon") + transform: [ + Scale { xScale: -1; origin.x: width/2 } + ] + } + + onClicked: redoAction.trigger() + } + + Cura.SecondaryButton + { + id: clearButton + text: catalog.i18nc("@button", "Clear all") + onClicked: UM.Controller.triggerAction("clear") + } + } + } + + Rectangle + { + id: waitPrepareItem + anchors.fill: parent + color: UM.Theme.getColor("main_background") + visible: UM.Controller.properties.getValue("State") === Cura.PaintToolState.PREPARING_MODEL + + ColumnLayout + { + anchors.fill: parent + + UM.Label + { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.verticalStretchFactor: 2 + + text: catalog.i18nc("@label", "Preparing model for painting...") + verticalAlignment: Text.AlignBottom + horizontalAlignment: Text.AlignHCenter + } + + Item + { + Layout.preferredWidth: loadingIndicator.width + Layout.alignment: Qt.AlignHCenter + Layout.fillHeight: true + Layout.verticalStretchFactor: 1 + + UM.ColorImage + { + id: loadingIndicator + + anchors.top: parent.top + anchors.left: parent.left + width: UM.Theme.getSize("card_icon").width + height: UM.Theme.getSize("card_icon").height + source: UM.Theme.getIcon("ArrowDoubleCircleRight") + color: UM.Theme.getColor("text_default") + + RotationAnimator + { + target: loadingIndicator + from: 0 + to: 360 + duration: 2000 + loops: Animation.Infinite + running: true + alwaysRunToEnd: true + } + } + } + } + } + + Rectangle + { + id: selectSingleMessageItem + anchors.fill: parent + color: UM.Theme.getColor("main_background") + visible: UM.Controller.properties.getValue("State") === Cura.PaintToolState.MULTIPLE_SELECTION + + UM.Label + { + anchors.fill: parent + text: catalog.i18nc("@label", "Select a single model to start painting") + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + } +} diff --git a/plugins/PaintTool/PaintUndoCommand.py b/plugins/PaintTool/PaintUndoCommand.py new file mode 100644 index 0000000000..50bfb787b7 --- /dev/null +++ b/plugins/PaintTool/PaintUndoCommand.py @@ -0,0 +1,104 @@ +# Copyright (c) 2025 UltiMaker +# Cura is released under the terms of the LGPLv3 or higher. + +from typing import cast, Optional + +from PyQt6.QtCore import QRect, QPoint +from PyQt6.QtGui import QUndoCommand, QImage, QPainter + +from UM.View.GL.Texture import Texture + + +class PaintUndoCommand(QUndoCommand): + """Provides the command that does the actual painting on objects with undo/redo mechanisms""" + + def __init__(self, + texture: Texture, + stroke_mask: QImage, + x: int, + y: int, + set_value: int, + bit_range: tuple[int, int], + mergeable: bool) -> None: + super().__init__() + + self._original_texture_image: Optional[QImage] = texture.getImage().copy() if not mergeable else None + self._texture: Texture = texture + self._stroke_mask: QImage = stroke_mask + self._x: int = x + self._y: int = y + self._set_value: int = set_value + self._bit_range: tuple[int, int] = bit_range + self._mergeable: bool = mergeable + + def id(self) -> int: + # Since the undo stack will contain only commands of this type, we can use a fixed ID + return 0 + + def redo(self) -> None: + actual_image = self._texture.getImage() + + bit_range_start, bit_range_end = self._bit_range + full_int32 = 0xffffffff + clear_texture_bit_mask = full_int32 ^ (((full_int32 << (32 - 1 - (bit_range_end - bit_range_start))) & full_int32) >> ( + 32 - 1 - bit_range_end)) + image_rect = QRect(0, 0, self._stroke_mask.width(), self._stroke_mask.height()) + + clear_bits_image = self._stroke_mask.copy() + clear_bits_image.invertPixels() + painter = QPainter(clear_bits_image) + painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Lighten) + painter.fillRect(image_rect, clear_texture_bit_mask) + painter.end() + + set_value_image = self._stroke_mask.copy() + painter = QPainter(set_value_image) + painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Multiply) + painter.fillRect(image_rect, self._set_value) + painter.end() + + stroked_image = actual_image.copy(self._x, self._y, self._stroke_mask.width(), self._stroke_mask.height()) + painter = QPainter(stroked_image) + painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceAndDestination) + painter.drawImage(0, 0, clear_bits_image) + painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceOrDestination) + painter.drawImage(0, 0, set_value_image) + painter.end() + + self._texture.setSubImage(stroked_image, self._x, self._y) + + def undo(self) -> None: + if self._original_texture_image is not None: + self._texture.setSubImage(self._original_texture_image.copy(self._x, + self._y, + self._stroke_mask.width(), + self._stroke_mask.height()), + self._x, + self._y) + + def mergeWith(self, command: QUndoCommand) -> bool: + if not isinstance(command, PaintUndoCommand): + return False + paint_undo_command = cast(PaintUndoCommand, command) + + if not paint_undo_command._mergeable: + return False + + self_rect = QRect(QPoint(self._x, self._y), self._stroke_mask.size()) + command_rect = QRect(QPoint(paint_undo_command._x, paint_undo_command._y), paint_undo_command._stroke_mask.size()) + bounding_rect = self_rect.united(command_rect) + + merged_mask = QImage(bounding_rect.width(), bounding_rect.height(), self._stroke_mask.format()) + merged_mask.fill(0) + + painter = QPainter(merged_mask) + painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Lighten) + painter.drawImage(self._x - bounding_rect.x(), self._y - bounding_rect.y(), self._stroke_mask) + painter.drawImage(paint_undo_command._x - bounding_rect.x(), paint_undo_command._y - bounding_rect.y(), paint_undo_command._stroke_mask) + painter.end() + + self._x = bounding_rect.x() + self._y = bounding_rect.y() + self._stroke_mask = merged_mask + + return True diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py new file mode 100644 index 0000000000..b3bc9867c3 --- /dev/null +++ b/plugins/PaintTool/PaintView.py @@ -0,0 +1,173 @@ +# Copyright (c) 2025 UltiMaker +# Cura is released under the terms of the LGPLv3 or higher. + +import os +from PyQt6.QtCore import QRect, pyqtSignal +from typing import Optional, Dict + +from PyQt6.QtGui import QImage, QUndoStack + +from cura.CuraApplication import CuraApplication +from cura.BuildVolume import BuildVolume +from cura.CuraView import CuraView +from UM.PluginRegistry import PluginRegistry +from UM.View.GL.ShaderProgram import ShaderProgram +from UM.View.GL.Texture import Texture +from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator +from UM.Scene.Selection import Selection +from UM.View.GL.OpenGL import OpenGL +from UM.i18n import i18nCatalog +from UM.Math.Color import Color + +from .PaintUndoCommand import PaintUndoCommand + +catalog = i18nCatalog("cura") + + +class PaintView(CuraView): + """View for model-painting.""" + + class PaintType: + def __init__(self, display_color: Color, value: int): + self.display_color: Color = display_color + self.value: int = value + + def __init__(self) -> None: + super().__init__(use_empty_menu_placeholder = True) + self._paint_shader: Optional[ShaderProgram] = None + self._current_paint_texture: Optional[Texture] = None + self._current_bits_ranges: tuple[int, int] = (0, 0) + self._current_paint_type = "" + self._paint_modes: Dict[str, Dict[str, "PaintView.PaintType"]] = {} + + self._paint_undo_stack: QUndoStack = QUndoStack() + self._paint_undo_stack.setUndoLimit(32) # Set a quite low amount since every command copies the full texture + self._paint_undo_stack.canUndoChanged.connect(self.canUndoChanged) + self._paint_undo_stack.canRedoChanged.connect(self.canRedoChanged) + + application = CuraApplication.getInstance() + application.engineCreatedSignal.connect(self._makePaintModes) + self._scene = application.getController().getScene() + + canUndoChanged = pyqtSignal(bool) + canRedoChanged = pyqtSignal(bool) + + def canUndo(self): + return self._paint_undo_stack.canUndo() + + def canRedo(self): + return self._paint_undo_stack.canRedo() + + def _makePaintModes(self): + theme = CuraApplication.getInstance().getTheme() + usual_types = {"none": self.PaintType(Color(*theme.getColor("paint_normal_area").getRgb()), 0), + "preferred": self.PaintType(Color(*theme.getColor("paint_preferred_area").getRgb()), 1), + "avoid": self.PaintType(Color(*theme.getColor("paint_avoid_area").getRgb()), 2)} + self._paint_modes = { + "seam": usual_types, + "support": usual_types, + } + + self._current_paint_type = "seam" + + def _checkSetup(self): + if not self._paint_shader: + shader_filename = os.path.join(PluginRegistry.getInstance().getPluginPath("PaintTool"), "paint.shader") + self._paint_shader = OpenGL.getInstance().createShaderProgram(shader_filename) + + def addStroke(self, stroke_mask: QImage, start_x: int, start_y: int, brush_color: str, merge_with_previous: bool) -> None: + if self._current_paint_texture is None or self._current_paint_texture.getImage() is None: + return + + self._prepareDataMapping() + + current_image = self._current_paint_texture.getImage() + texture_rect = QRect(0, 0, current_image.width(), current_image.height()) + stroke_rect = QRect(start_x, start_y, stroke_mask.width(), stroke_mask.height()) + intersect_rect = texture_rect.intersected(stroke_rect) + if intersect_rect != stroke_rect: + # Stroke doesn't fully fit into the image, we have to crop it + stroke_mask = stroke_mask.copy(intersect_rect.x() - start_x, + intersect_rect.y() - start_y, + intersect_rect.width(), + intersect_rect.height()) + start_x = intersect_rect.x() + start_y = intersect_rect.y() + + bit_range_start, bit_range_end = self._current_bits_ranges + set_value = self._paint_modes[self._current_paint_type][brush_color].value << bit_range_start + + self._paint_undo_stack.push(PaintUndoCommand(self._current_paint_texture, + stroke_mask, + start_x, + start_y, + set_value, + (bit_range_start, bit_range_end), + merge_with_previous)) + + def undoStroke(self) -> None: + self._paint_undo_stack.undo() + + def redoStroke(self) -> None: + self._paint_undo_stack.redo() + + def getUvTexDimensions(self): + if self._current_paint_texture is not None: + return self._current_paint_texture.getWidth(), self._current_paint_texture.getHeight() + return 0, 0 + + def getPaintType(self) -> str: + return self._current_paint_type + + def setPaintType(self, paint_type: str) -> None: + self._current_paint_type = paint_type + + def _prepareDataMapping(self): + node = Selection.getAllSelectedObjects()[0] + if node is None: + return + + paint_data_mapping = node.callDecoration("getTextureDataMapping") + + if self._current_paint_type not in paint_data_mapping: + new_mapping = self._add_mapping(paint_data_mapping, len(self._paint_modes[self._current_paint_type])) + paint_data_mapping[self._current_paint_type] = new_mapping + node.callDecoration("setTextureDataMapping", paint_data_mapping) + + self._current_bits_ranges = paint_data_mapping[self._current_paint_type] + + @staticmethod + def _add_mapping(actual_mapping: Dict[str, tuple[int, int]], nb_storable_values: int) -> tuple[int, int]: + start_index = 0 + if actual_mapping: + start_index = max(end_index for _, end_index in actual_mapping.values()) + 1 + + end_index = start_index + int.bit_length(nb_storable_values - 1) - 1 + + return start_index, end_index + + def beginRendering(self) -> None: + if self._current_paint_type not in self._paint_modes: + return + + self._checkSetup() + renderer = self.getRenderer() + + for node in DepthFirstIterator(self._scene.getRoot()): + if isinstance(node, BuildVolume): + node.render(renderer) + + paint_batch = renderer.createRenderBatch(shader=self._paint_shader) + renderer.addRenderBatch(paint_batch) + + for node in Selection.getAllSelectedObjects(): + paint_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData(), normal_transformation=node.getCachedNormalMatrix()) + self._current_paint_texture = node.callDecoration("getPaintTexture") + self._paint_shader.setTexture(0, self._current_paint_texture) + + self._paint_shader.setUniformValue("u_bitsRangesStart", self._current_bits_ranges[0]) + self._paint_shader.setUniformValue("u_bitsRangesEnd", self._current_bits_ranges[1]) + + colors = [paint_type_obj.display_color for paint_type_obj in self._paint_modes[self._current_paint_type].values()] + colors_values = [[int(color_part * 255) for color_part in [color.r, color.g, color.b]] for color in colors] + self._paint_shader.setUniformValueArray("u_renderColors", colors_values) diff --git a/plugins/PaintTool/PrepareTextureJob.py b/plugins/PaintTool/PrepareTextureJob.py new file mode 100644 index 0000000000..6c5e61c009 --- /dev/null +++ b/plugins/PaintTool/PrepareTextureJob.py @@ -0,0 +1,33 @@ +# Copyright (c) 2025 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from UM.Job import Job +from UM.Scene.SceneNode import SceneNode +from UM.View.GL.OpenGL import OpenGL + + +class PrepareTextureJob(Job): + """ + Background job to prepare a model for painting, i.e. do the UV-unwrapping and create the appropriate texture image, + which can last a few seconds + """ + + def __init__(self, node: SceneNode): + super().__init__() + self._node: SceneNode = node + + def run(self) -> None: + # If the model has already-provided UV coordinates, we can only assume that the associated texture + # should be a square + texture_width = texture_height = 4096 + + mesh = self._node.getMeshData() + if not mesh.hasUVCoordinates(): + texture_width, texture_height = mesh.calculateUnwrappedUVCoordinates() + + self._node.callDecoration("prepareTexture", texture_width, texture_height) + + if hasattr(mesh, OpenGL.VertexBufferProperty): + # Force clear OpenGL buffer so that new UV coordinates will be sent + delattr(mesh, OpenGL.VertexBufferProperty) + diff --git a/plugins/PaintTool/__init__.py b/plugins/PaintTool/__init__.py new file mode 100644 index 0000000000..a95559ff0f --- /dev/null +++ b/plugins/PaintTool/__init__.py @@ -0,0 +1,35 @@ +# Copyright (c) 2025 UltiMaker +# Cura is released under the terms of the LGPLv3 or higher. + +from . import PaintTool +from . import PaintView + +from PyQt6.QtQml import qmlRegisterUncreatableType + +from UM.i18n import i18nCatalog +i18n_catalog = i18nCatalog("cura") + +def getMetaData(): + return { + "tool": { + "name": i18n_catalog.i18nc("@action:button", "Paint"), + "description": i18n_catalog.i18nc("@info:tooltip", "Paint Model"), + "icon": "Visual", + "tool_panel": "PaintTool.qml", + "weight": 0 + }, + "view": { + "name": i18n_catalog.i18nc("@item:inmenu", "Paint view"), + "weight": 0, + "visible": False + } + } + +def register(app): + qmlRegisterUncreatableType(PaintTool.PaintTool.Brush, "Cura", 1, 0, "This is an enumeration class", "PaintToolBrush") + qmlRegisterUncreatableType(PaintTool.PaintTool.Paint, "Cura", 1, 0, "This is an enumeration class", "PaintToolState") + view = PaintView.PaintView() + return { + "tool": PaintTool.PaintTool(view), + "view": view + } diff --git a/plugins/PaintTool/paint.shader b/plugins/PaintTool/paint.shader new file mode 100644 index 0000000000..1982724910 --- /dev/null +++ b/plugins/PaintTool/paint.shader @@ -0,0 +1,146 @@ +[shaders] +vertex = + uniform highp mat4 u_modelMatrix; + uniform highp mat4 u_viewMatrix; + uniform highp mat4 u_projectionMatrix; + + uniform highp mat4 u_normalMatrix; + + attribute highp vec4 a_vertex; + attribute highp vec4 a_normal; + attribute highp vec2 a_uvs; + + varying highp vec3 v_vertex; + varying highp vec3 v_normal; + varying highp vec2 v_uvs; + + void main() + { + vec4 world_space_vert = u_modelMatrix * a_vertex; + gl_Position = u_projectionMatrix * u_viewMatrix * world_space_vert; + + v_vertex = world_space_vert.xyz; + v_normal = (u_normalMatrix * normalize(a_normal)).xyz; + + v_uvs = a_uvs; + } + +fragment = + uniform mediump vec4 u_ambientColor; + uniform highp vec3 u_lightPosition; + uniform highp vec3 u_viewPosition; + uniform sampler2D u_texture; + uniform mediump int u_bitsRangesStart; + uniform mediump int u_bitsRangesEnd; + uniform mediump vec3 u_renderColors[16]; + + varying highp vec3 v_vertex; + varying highp vec3 v_normal; + varying highp vec2 v_uvs; + + void main() + { + mediump vec4 final_color = vec4(0.0); + + /* Ambient Component */ + final_color += u_ambientColor; + + highp vec3 normal = normalize(v_normal); + highp vec3 light_dir = normalize(u_lightPosition - v_vertex); + + /* Diffuse Component */ + ivec4 texture = ivec4(texture(u_texture, v_uvs) * 255.0); + uint color_index = (texture.r << 16) | (texture.g << 8) | texture.b; + color_index = (color_index << (32 - 1 - u_bitsRangesEnd)) >> 32 - 1 - (u_bitsRangesEnd - u_bitsRangesStart); + + vec4 diffuse_color = vec4(u_renderColors[color_index] / 255.0, 1.0); + highp float n_dot_l = mix(0.3, 0.7, dot(normal, light_dir)); + final_color += (n_dot_l * diffuse_color); + + final_color.a = 1.0; + + frag_color = final_color; + } + +vertex41core = + #version 410 + uniform highp mat4 u_modelMatrix; + uniform highp mat4 u_viewMatrix; + uniform highp mat4 u_projectionMatrix; + + uniform highp mat4 u_normalMatrix; + + in highp vec4 a_vertex; + in highp vec4 a_normal; + in highp vec2 a_uvs; + + out highp vec3 v_vertex; + out highp vec3 v_normal; + out highp vec2 v_uvs; + + void main() + { + vec4 world_space_vert = u_modelMatrix * a_vertex; + gl_Position = u_projectionMatrix * u_viewMatrix * world_space_vert; + + v_vertex = world_space_vert.xyz; + v_normal = (u_normalMatrix * normalize(a_normal)).xyz; + + v_uvs = a_uvs; + } + +fragment41core = + #version 410 + uniform mediump vec4 u_ambientColor; + uniform highp vec3 u_lightPosition; + uniform highp vec3 u_viewPosition; + uniform sampler2D u_texture; + uniform mediump int u_bitsRangesStart; + uniform mediump int u_bitsRangesEnd; + uniform mediump vec3 u_renderColors[16]; + + in highp vec3 v_vertex; + in highp vec3 v_normal; + in highp vec2 v_uvs; + out vec4 frag_color; + + void main() + { + mediump vec4 final_color = vec4(0.0); + + /* Ambient Component */ + final_color += u_ambientColor; + + highp vec3 normal = normalize(v_normal); + highp vec3 light_dir = normalize(u_lightPosition - v_vertex); + + /* Diffuse Component */ + ivec4 texture = ivec4(texture(u_texture, v_uvs) * 255.0); + uint color_index = (texture.r << 16) | (texture.g << 8) | texture.b; + color_index = (color_index << (32 - 1 - u_bitsRangesEnd)) >> 32 - 1 - (u_bitsRangesEnd - u_bitsRangesStart); + + vec4 diffuse_color = vec4(u_renderColors[color_index] / 255.0, 1.0); + highp float n_dot_l = mix(0.3, 0.7, dot(normal, light_dir)); + final_color += (n_dot_l * diffuse_color); + + final_color.a = 1.0; + + frag_color = final_color; + } + +[defaults] +u_ambientColor = [0.3, 0.3, 0.3, 1.0] +u_texture = 0 + +[bindings] +u_modelMatrix = model_matrix +u_viewMatrix = view_matrix +u_projectionMatrix = projection_matrix +u_normalMatrix = normal_matrix +u_lightPosition = light_0_position +u_viewPosition = camera_position + +[attributes] +a_vertex = vertex +a_normal = normal +a_uvs = uv0 diff --git a/plugins/PaintTool/plugin.json b/plugins/PaintTool/plugin.json new file mode 100644 index 0000000000..2a55d677d2 --- /dev/null +++ b/plugins/PaintTool/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "Paint Tools", + "author": "UltiMaker", + "version": "1.0.0", + "description": "Provides the paint tools.", + "api": 8, + "i18n-catalog": "cura" +} diff --git a/plugins/PreviewStage/PreviewStage.py b/plugins/PreviewStage/PreviewStage.py index 88f432ef9b..3f1a4423b2 100644 --- a/plugins/PreviewStage/PreviewStage.py +++ b/plugins/PreviewStage/PreviewStage.py @@ -24,25 +24,6 @@ class PreviewStage(CuraStage): super().__init__(parent) self._application = application self._application.engineCreatedSignal.connect(self._engineCreated) - self._previously_active_view = None # type: Optional[View] - - def onStageSelected(self) -> None: - """When selecting the stage, remember which was the previous view so that - - we can revert to that view when we go out of the stage later. - """ - self._previously_active_view = self._application.getController().getActiveView() - - def onStageDeselected(self) -> None: - """Called when going to a different stage (away from the Preview Stage). - - When going to a different stage, the view should be reverted to what it - was before. Normally, that just reverts it to solid view. - """ - - if self._previously_active_view is not None: - self._application.getController().setActiveView(self._previously_active_view.getPluginId()) - self._previously_active_view = None def _engineCreated(self) -> None: """Delayed load of the QML files. diff --git a/plugins/SimulationView/SimulationPass.py b/plugins/SimulationView/SimulationPass.py index 436d5b8723..a7411a2ed0 100644 --- a/plugins/SimulationView/SimulationPass.py +++ b/plugins/SimulationView/SimulationPass.py @@ -203,9 +203,9 @@ class SimulationPass(RenderPass): self._layer_shader.setUniformValue("u_next_vertex", not_a_vector) self._layer_shader.setUniformValue("u_last_line_ratio", 1.0) - # The first line does not have a previous line: add a MoveCombingType in front for start detection + # The first line does not have a previous line: add a MoveUnretractedType in front for start detection # this way the first start of the layer can also be drawn - prev_line_types = numpy.concatenate([numpy.asarray([LayerPolygon.MoveCombingType], dtype = numpy.float32), layer_data._attributes["line_types"]["value"]]) + prev_line_types = numpy.concatenate([numpy.asarray([LayerPolygon.MoveUnretractedType], dtype = numpy.float32), layer_data._attributes["line_types"]["value"]]) # Remove the last element prev_line_types = prev_line_types[0:layer_data._attributes["line_types"]["value"].size] layer_data._attributes["prev_line_types"] = {'opengl_type': 'float', 'value': prev_line_types, 'opengl_name': 'a_prev_line_type'} diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index 10861acfd0..5d339e7f74 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -172,13 +172,20 @@ class SimulationView(CuraView): self._updateSliceWarningVisibility() self.activityChanged.emit() - def getSimulationPass(self) -> SimulationPass: + def getSimulationPass(self) -> Optional[SimulationPass]: if not self._layer_pass: + renderer = self.getRenderer() + if renderer is None: + return None + # Currently the RenderPass constructor requires a size > 0 # This should be fixed in RenderPass's constructor. self._layer_pass = SimulationPass(1, 1) self._compatibility_mode = self._evaluateCompatibilityMode() self._layer_pass.setSimulationView(self) + self._layer_pass.setEnabled(False) + renderer.addRenderPass(self._layer_pass) + return self._layer_pass def getCurrentLayer(self) -> int: @@ -608,8 +615,10 @@ class SimulationView(CuraView): visible_line_types.append(LayerPolygon.SupportInterfaceType) visible_line_types_with_extrusion = visible_line_types.copy() # Copy before travel moves are added if self.getShowTravelMoves(): - visible_line_types.append(LayerPolygon.MoveCombingType) - visible_line_types.append(LayerPolygon.MoveRetractionType) + visible_line_types.append(LayerPolygon.MoveUnretractedType) + visible_line_types.append(LayerPolygon.MoveRetractedType) + visible_line_types.append(LayerPolygon.MoveWhileRetractingType) + visible_line_types.append(LayerPolygon.MoveWhileUnretractingType) for node in DepthFirstIterator(self.getController().getScene().getRoot()): layer_data = node.callDecoration("getLayerData") @@ -732,11 +741,14 @@ class SimulationView(CuraView): # Make sure the SimulationPass is created layer_pass = self.getSimulationPass() + if layer_pass is None: + return False + renderer = self.getRenderer() if renderer is None: return False - renderer.addRenderPass(layer_pass) + layer_pass.setEnabled(True) # Make sure the NozzleNode is add to the root nozzle = self.getNozzleNode() @@ -776,7 +788,7 @@ class SimulationView(CuraView): return False if self._layer_pass is not None: - renderer.removeRenderPass(self._layer_pass) + self._layer_pass.setEnabled(False) if self._composite_pass: self._composite_pass.setLayerBindings(cast(List[str], self._old_layer_bindings)) self._composite_pass.setCompositeShader(cast(ShaderProgram, self._old_composite_shader)) diff --git a/plugins/SimulationView/SimulationViewMenuComponent.qml b/plugins/SimulationView/SimulationViewMenuComponent.qml index d434d883eb..78b0b2b74f 100644 --- a/plugins/SimulationView/SimulationViewMenuComponent.qml +++ b/plugins/SimulationView/SimulationViewMenuComponent.qml @@ -227,29 +227,52 @@ Cura.ExpandableComponent id: typesLegendModel Component.onCompleted: { + const travelsTypesModel = [ + { + label: catalog.i18nc("@label", "Not retracted"), + colorId: "layerview_move_combing" + }, + { + label: catalog.i18nc("@label", "Retracted"), + colorId: "layerview_move_retraction" + }, + { + label: catalog.i18nc("@label", "Retracting"), + colorId: "layerview_move_while_retracting" + }, + { + label: catalog.i18nc("@label", "Priming"), + colorId: "layerview_move_while_unretracting" + } + ]; + typesLegendModel.append({ label: catalog.i18nc("@label", "Travels"), initialValue: viewSettings.show_travel_moves, preference: "layerview/show_travel_moves", - colorId: "layerview_move_combing" + colorId: "layerview_move_combing", + subTypesModel: travelsTypesModel }); typesLegendModel.append({ label: catalog.i18nc("@label", "Helpers"), initialValue: viewSettings.show_helpers, preference: "layerview/show_helpers", - colorId: "layerview_support" + colorId: "layerview_support", + subTypesModel: [] }); typesLegendModel.append({ label: catalog.i18nc("@label", "Shell"), initialValue: viewSettings.show_skin, preference: "layerview/show_skin", - colorId: "layerview_inset_0" + colorId: "layerview_inset_0", + subTypesModel: [] }); typesLegendModel.append({ label: catalog.i18nc("@label", "Infill"), initialValue: viewSettings.show_infill, preference: "layerview/show_infill", - colorId: "layerview_infill" + colorId: "layerview_infill", + subTypesModel: [] }); if (! UM.SimulationView.compatibilityMode) { @@ -257,7 +280,8 @@ Cura.ExpandableComponent label: catalog.i18nc("@label", "Starts"), initialValue: viewSettings.show_starts, preference: "layerview/show_starts", - colorId: "layerview_starts" + colorId: "layerview_starts", + subTypesModel: [] }); } } @@ -273,6 +297,7 @@ Cura.ExpandableComponent Rectangle { + id: rectangleColor anchors.verticalCenter: parent.verticalCenter anchors.right: legendModelCheckBox.right width: UM.Theme.getSize("layerview_legend_size").width @@ -281,6 +306,58 @@ Cura.ExpandableComponent border.width: UM.Theme.getSize("default_lining").width border.color: UM.Theme.getColor("lining") visible: viewSettings.show_legend + + MouseArea + { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + enabled: subTypesModel.count > 0 + + onEntered: tooltip.show() + onExited: tooltip.hide() + + UM.ToolTip + { + id: tooltip + delay: 0 + width: subTypesColumn.implicitWidth + 2 * UM.Theme.getSize("thin_margin").width + height: subTypesColumn.implicitHeight + 2 * UM.Theme.getSize("thin_margin").width + + contentItem: Column + { + id: subTypesColumn + padding: 0 + spacing: UM.Theme.getSize("layerview_row_spacing").height + + Repeater + { + model: subTypesModel + UM.Label + { + text: label + + height: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height + width: UM.Theme.getSize("layerview_menu_size").width + color: UM.Theme.getColor("tooltip_text") + Rectangle + { + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + + width: UM.Theme.getSize("layerview_legend_size").width + height: UM.Theme.getSize("layerview_legend_size").height + + color: UM.Theme.getColor(model.colorId) + + border.width: UM.Theme.getSize("default_lining").width + border.color: UM.Theme.getColor("lining") + } + } + } + } + } + } } UM.Label diff --git a/plugins/SimulationView/layers.shader b/plugins/SimulationView/layers.shader index e6210c2b65..d5079fd82b 100644 --- a/plugins/SimulationView/layers.shader +++ b/plugins/SimulationView/layers.shader @@ -22,8 +22,8 @@ vertex = gl_Position = u_projectionMatrix * u_viewMatrix * u_modelMatrix * a_vertex; // shade the color depending on the extruder index v_color = a_color; - // 8 and 9 are travel moves - if ((a_line_type != 8.0) && (a_line_type != 9.0)) { + // 8, 9, 12 and 13 are travel moves + if ((a_line_type != 8.0) && (a_line_type != 9.0) && (a_line_type != 12.0) && (a_line_type != 13.0)) { v_color = (a_extruder == u_active_extruder) ? v_color : vec4(u_shade_factor * v_color.rgb, v_color.a); } @@ -48,7 +48,9 @@ fragment = void main() { - if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5)) { // actually, 8 and 9 + // travel moves: 8, 9, 12, 13 + if ((u_show_travel_moves == 0) && (((v_line_type >= 7.5) && (v_line_type <= 9.5)) || + ((v_line_type >= 11.5) && (v_line_type <= 13.5)))) { // discard movements discard; } @@ -100,7 +102,7 @@ vertex41core = { gl_Position = u_projectionMatrix * u_viewMatrix * u_modelMatrix * a_vertex; v_color = a_color; - if ((a_line_type != 8) && (a_line_type != 9)) { + if ((a_line_type != 8) && (a_line_type != 9) && (a_line_type != 12) && (a_line_type != 13)) { v_color = (a_extruder == u_active_extruder) ? v_color : vec4(u_shade_factor * v_color.rgb, v_color.a); } @@ -120,7 +122,9 @@ fragment41core = void main() { - if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5)) { // actually, 8 and 9 + // travel moves: 8, 9, 12, 13 + if ((u_show_travel_moves == 0) && (((v_line_type >= 7.5) && (v_line_type <= 9.5)) || + ((v_line_type >= 11.5) && (v_line_type <= 13.5)))) { // discard movements discard; } diff --git a/plugins/SimulationView/layers3d.shader b/plugins/SimulationView/layers3d.shader index 494a07083d..e2f57823f3 100644 --- a/plugins/SimulationView/layers3d.shader +++ b/plugins/SimulationView/layers3d.shader @@ -228,22 +228,26 @@ geometry41core = { highp mat4 viewProjectionMatrix = u_projectionMatrix * u_viewMatrix; - vec4 g_vertex_delta; - vec3 g_vertex_normal_horz; // horizontal and vertical in respect to layers - vec4 g_vertex_offset_horz; // vec4 to match gl_in[x].gl_Position + // Vertices are declared as vec4 so that they can be used for calculations with gl_in[x].gl_Position + vec3 g_vertex_delta; + vec3 g_vertex_normal_horz; + vec4 g_vertex_offset_horz; vec3 g_vertex_normal_vert; vec4 g_vertex_offset_vert; vec3 g_vertex_normal_horz_head; vec4 g_vertex_offset_horz_head; + vec3 g_axial_plan_vector; + vec3 g_radial_plan_vector; float size_x; float size_y; - if ((v_extruder_opacity[0][int(mod(v_extruder[0], 4))][v_extruder[0] / 4] == 0.0) && (v_line_type[0] != 8) && (v_line_type[0] != 9)) { + if ((v_extruder_opacity[0][int(mod(v_extruder[0], 4))][v_extruder[0] / 4] == 0.0) && + (v_line_type[0] != 8) && (v_line_type[0] != 9) && (v_line_type[0] != 12) && (v_line_type[0] != 13)) { return; } - // See LayerPolygon; 8 is MoveCombingType, 9 is RetractionType - if ((u_show_travel_moves == 0) && ((v_line_type[0] == 8) || (v_line_type[0] == 9))) { + // See LayerPolygon; 8 is MoveUnretractedType, 9 is RetractionType, 12 is MoveWhileRetractingType, 13 is MoveWhileUnretractingType + if ((u_show_travel_moves == 0) && ((v_line_type[0] == 8) || (v_line_type[0] == 9) || (v_line_type[0] == 12) || (v_line_type[0] == 13))) { return; } if ((u_show_helpers == 0) && ((v_line_type[0] == 4) || (v_line_type[0] == 5) || (v_line_type[0] == 7) || (v_line_type[0] == 10) || v_line_type[0] == 11)) { @@ -256,7 +260,7 @@ geometry41core = return; } - if ((v_line_type[0] == 8) || (v_line_type[0] == 9)) { + if ((v_line_type[0] == 8) || (v_line_type[0] == 9) || (v_line_type[0] == 12) || (v_line_type[0] == 13)) { // fixed size for movements size_x = 0.05; } else { @@ -264,26 +268,47 @@ geometry41core = } size_y = v_line_dim[1].y / 2 + 0.01; - g_vertex_delta = gl_in[1].gl_Position - gl_in[0].gl_Position; //Actual movement exhibited by the line. - g_vertex_normal_horz_head = normalize(vec3(-g_vertex_delta.x, -g_vertex_delta.y, -g_vertex_delta.z)); //Lengthwise normal vector pointing backwards. - g_vertex_offset_horz_head = vec4(g_vertex_normal_horz_head * size_x, 0.0); //Lengthwise offset vector pointing backwards. + g_vertex_delta = (gl_in[1].gl_Position - gl_in[0].gl_Position).xyz; //Actual movement exhibited by the line. - g_vertex_normal_horz = normalize(vec3(g_vertex_delta.z, g_vertex_delta.y, -g_vertex_delta.x)); //Normal vector pointing right. + if (g_vertex_delta == vec3(0.0)) { + return; + } + + if (g_vertex_delta.y == 0.0) + { + // vector is in the horizontal plan, radial vector is a simple rotation around Y axis + g_radial_plan_vector = vec3(g_vertex_delta.z, 0.0, -g_vertex_delta.x); + } + else if(g_vertex_delta.x == 0.0 && g_vertex_delta.z == 0.0) + { + // delta vector is purely vertical, display the line rotated vertically so that it is visible in front and side views + g_radial_plan_vector = vec3(1.0, 0.0, -1.0); + } + else + { + // delta vector is completely 3D + g_axial_plan_vector = vec3(g_vertex_delta.x, 0.0, g_vertex_delta.z); // Vector projected in the horizontal plan + g_radial_plan_vector = cross(g_vertex_delta, g_axial_plan_vector); // Radial vector in the horizontal plan, pointing right. + } + + g_vertex_normal_horz_head = normalize(g_vertex_delta); //Lengthwise normal vector + g_vertex_offset_horz_head = vec4(g_vertex_normal_horz_head * size_x, 0.0); //Lengthwise offset vector + + g_vertex_normal_horz = normalize(g_radial_plan_vector); //Normal vector pointing right. g_vertex_offset_horz = vec4(g_vertex_normal_horz * size_x, 0.0); //Offset vector pointing right. g_vertex_normal_vert = vec3(0.0, 1.0, 0.0); //Upwards normal vector. g_vertex_offset_vert = vec4(g_vertex_normal_vert * size_y, 0.0); //Upwards offset vector. Goes up by half the layer thickness. - if ((v_line_type[0] == 8) || (v_line_type[0] == 9)) { //Travel or retraction moves. - vec4 va_head = viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head + g_vertex_offset_vert); + if ((v_line_type[0] == 8) || (v_line_type[0] == 9) || (v_line_type[0] == 12) || (v_line_type[0] == 13)) { //Travel or retraction moves. + vec4 va_head = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz_head + g_vertex_offset_vert); vec4 va_up = viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert); vec4 va_down = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert); - vec4 vb_head = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head + g_vertex_offset_vert); + vec4 vb_head = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz_head + g_vertex_offset_vert); vec4 vb_down = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert); vec4 vb_up = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert); // Travels: flat plane with pointy ends - myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_up); myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_head); myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_down); myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_up); @@ -308,8 +333,8 @@ geometry41core = vec4 vb_p_horz = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz); //Line end, right vertex. vec4 va_m_vert = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert); //Line start, bottom vertex. vec4 vb_m_vert = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert); //Line end, bottom vertex. - vec4 va_head = viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head); //Line start, tip. - vec4 vb_head = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head); //Line end, tip. + vec4 va_head = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz_head); //Line start, tip. + vec4 vb_head = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz_head); //Line end, tip. // All normal lines are rendered as 3d tubes. myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz, va_m_horz); @@ -328,14 +353,14 @@ geometry41core = // left side myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz, va_m_horz); myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_p_vert); - myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_horz_head, va_head); + myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz_head, va_head); myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_horz, va_p_horz); EndPrimitive(); myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_horz, va_p_horz); myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_vert, va_m_vert); - myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_horz_head, va_head); + myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz_head, va_head); myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz, va_m_horz); EndPrimitive(); @@ -343,14 +368,14 @@ geometry41core = // right side myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, vb_p_horz); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, vb_p_vert); - myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz_head, vb_head); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz_head, vb_head); myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, vb_m_horz); EndPrimitive(); myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, vb_m_horz); myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, vb_m_vert); - myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz_head, vb_head); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz_head, vb_head); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, vb_p_horz); EndPrimitive(); diff --git a/plugins/SimulationView/layers3d_shadow.shader b/plugins/SimulationView/layers3d_shadow.shader index 88268938c9..0cf3e4f75a 100644 --- a/plugins/SimulationView/layers3d_shadow.shader +++ b/plugins/SimulationView/layers3d_shadow.shader @@ -95,22 +95,26 @@ geometry41core = { highp mat4 viewProjectionMatrix = u_projectionMatrix * u_viewMatrix; - vec4 g_vertex_delta; - vec3 g_vertex_normal_horz; // horizontal and vertical in respect to layers - vec4 g_vertex_offset_horz; // vec4 to match gl_in[x].gl_Position + // Vertices are declared as vec4 so that they can be used for calculations with gl_in[x].gl_Position + vec3 g_vertex_delta; + vec3 g_vertex_normal_horz; + vec4 g_vertex_offset_horz; vec3 g_vertex_normal_vert; vec4 g_vertex_offset_vert; vec3 g_vertex_normal_horz_head; vec4 g_vertex_offset_horz_head; + vec3 g_axial_plane_vector; + vec3 g_radial_plane_vector; float size_x; float size_y; - if ((v_extruder_opacity[0][int(mod(v_extruder[0], 4))][v_extruder[0] / 4] == 0.0) && (v_line_type[0] != 8) && (v_line_type[0] != 9)) { + if ((v_extruder_opacity[0][int(mod(v_extruder[0], 4))][v_extruder[0] / 4] == 0.0) && + (v_line_type[0] != 8) && (v_line_type[0] != 9) && (v_line_type[0] != 12) && (v_line_type[0] != 13)) { return; } - // See LayerPolygon; 8 is MoveCombingType, 9 is RetractionType - if ((u_show_travel_moves == 0) && ((v_line_type[0] == 8) || (v_line_type[0] == 9))) { + // See LayerPolygon; 8 is MoveUnretractedType, 9 is RetractionType, 12 is MoveWhileRetractingType, 13 is MoveWhileUnretractingType + if ((u_show_travel_moves == 0) && ((v_line_type[0] == 8) || (v_line_type[0] == 9) || (v_line_type[0] == 12) || (v_line_type[0] == 13))) { return; } if ((u_show_helpers == 0) && ((v_line_type[0] == 4) || (v_line_type[0] == 5) || (v_line_type[0] == 7) || (v_line_type[0] == 10))) { @@ -123,7 +127,7 @@ geometry41core = return; } - if ((v_line_type[0] == 8) || (v_line_type[0] == 9)) { + if ((v_line_type[0] == 8) || (v_line_type[0] == 9) || (v_line_type[0] == 12) || (v_line_type[0] == 13)) { // fixed size for movements size_x = 0.05; } else { @@ -131,93 +135,114 @@ geometry41core = } size_y = v_line_dim[1].y / 2 + 0.01; - g_vertex_delta = gl_in[1].gl_Position - gl_in[0].gl_Position; - g_vertex_normal_horz_head = normalize(vec3(-g_vertex_delta.x, -g_vertex_delta.y, -g_vertex_delta.z)); - g_vertex_offset_horz_head = vec4(g_vertex_normal_horz_head * size_x, 0.0); + g_vertex_delta = (gl_in[1].gl_Position - gl_in[0].gl_Position).xyz; //Actual movement exhibited by the line. - g_vertex_normal_horz = normalize(vec3(g_vertex_delta.z, g_vertex_delta.y, -g_vertex_delta.x)); + if (g_vertex_delta == vec3(0.0)) { + return; + } - g_vertex_offset_horz = vec4(g_vertex_normal_horz * size_x, 0.0); //size * g_vertex_normal_horz; - g_vertex_normal_vert = vec3(0.0, 1.0, 0.0); - g_vertex_offset_vert = vec4(g_vertex_normal_vert * size_y, 0.0); + if (g_vertex_delta.y == 0.0) + { + // vector is in the horizontal plane, radial vector is a simple rotation around Y axis + g_radial_plane_vector = vec3(g_vertex_delta.z, 0.0, -g_vertex_delta.x); + } + else if(g_vertex_delta.x == 0.0 && g_vertex_delta.z == 0.0) + { + // delta vector is purely vertical, display the line rotated vertically so that it is visible in front and side views + g_radial_plane_vector = vec3(1.0, 0.0, -1.0); + } + else + { + // delta vector is completely 3D + g_axial_plane_vector = vec3(g_vertex_delta.x, 0.0, g_vertex_delta.z); // Vector projected in the horizontal plane + g_radial_plane_vector = cross(g_vertex_delta, g_axial_plane_vector); // Radial vector in the horizontal plane, pointing right. + } - if ((v_line_type[0] == 8) || (v_line_type[0] == 9)) { - vec4 va_head = viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head + g_vertex_offset_vert); + g_vertex_normal_horz_head = normalize(g_vertex_delta); //Lengthwise normal vector + g_vertex_offset_horz_head = vec4(g_vertex_normal_horz_head * size_x, 0.0); //Lengthwise offset vector + + g_vertex_normal_horz = normalize(g_radial_plane_vector); //Normal vector pointing right. + g_vertex_offset_horz = vec4(g_vertex_normal_horz * size_x, 0.0); //Offset vector pointing right. + + g_vertex_normal_vert = vec3(0.0, 1.0, 0.0); //Upwards normal vector. + g_vertex_offset_vert = vec4(g_vertex_normal_vert * size_y, 0.0); //Upwards offset vector. Goes up by half the layer thickness. + + if ((v_line_type[0] == 8) || (v_line_type[0] == 9) || (v_line_type[0] == 12) || (v_line_type[0] == 13)) { + vec4 va_head = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz_head + g_vertex_offset_vert); vec4 va_up = viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert); vec4 va_down = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert); - vec4 vb_head = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head + g_vertex_offset_vert); + vec4 vb_head = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz_head + g_vertex_offset_vert); vec4 vb_down = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert); vec4 vb_up = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert); // Travels: flat plane with pointy ends - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, va_up); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, va_head); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, va_down); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, va_up); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_head); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_down); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_up); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, vb_down); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, vb_up); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, vb_head); //And reverse so that the line is also visible from the back side. myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, vb_up); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, vb_down); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, va_up); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, va_down); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, va_head); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, va_up); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_up); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_down); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_head); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_up); EndPrimitive(); } else { - vec4 va_m_horz = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz); - vec4 vb_m_horz = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz); - vec4 va_p_vert = viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert); - vec4 vb_p_vert = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert); - vec4 va_p_horz = viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz); - vec4 vb_p_horz = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz); - vec4 va_m_vert = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert); - vec4 vb_m_vert = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert); - vec4 va_head = viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head); - vec4 vb_head = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head); + vec4 va_m_horz = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz); //Line start, left vertex. + vec4 vb_m_horz = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz); //Line end, left vertex. + vec4 va_p_vert = viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert); //Line start, top vertex. + vec4 vb_p_vert = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert); //Line end, top vertex. + vec4 va_p_horz = viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz); //Line start, right vertex. + vec4 vb_p_horz = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz); //Line end, right vertex. + vec4 va_m_vert = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert); //Line start, bottom vertex. + vec4 vb_m_vert = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert); //Line end, bottom vertex. + vec4 va_head = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz_head); //Line start, tip. + vec4 vb_head = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz_head); //Line end, tip. // All normal lines are rendered as 3d tubes. - myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, va_m_horz); + myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz, va_m_horz); myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, vb_m_horz); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, va_p_vert); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_p_vert); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, vb_p_vert); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, va_p_horz); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_horz, va_p_horz); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, vb_p_horz); - myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, va_m_vert); + myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_vert, va_m_vert); myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, vb_m_vert); - myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, va_m_horz); + myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz, va_m_horz); myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, vb_m_horz); EndPrimitive(); // left side - myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, va_m_horz); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, va_p_vert); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, va_head); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, va_p_horz); + myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz, va_m_horz); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_p_vert); + myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz_head, va_head); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_horz, va_p_horz); EndPrimitive(); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, va_p_horz); - myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, va_m_vert); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, va_head); - myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, va_m_horz); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_horz, va_p_horz); + myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_vert, va_m_vert); + myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz_head, va_head); + myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz, va_m_horz); EndPrimitive(); // right side myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, vb_p_horz); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, vb_p_vert); - myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz_head, vb_head); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz_head, vb_head); myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, vb_m_horz); EndPrimitive(); myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, vb_m_horz); myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, vb_m_vert); - myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz_head, vb_head); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz_head, vb_head); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, vb_p_horz); EndPrimitive(); diff --git a/plugins/SimulationView/layers_shadow.shader b/plugins/SimulationView/layers_shadow.shader index 4bc2de3d0b..73278914b7 100644 --- a/plugins/SimulationView/layers_shadow.shader +++ b/plugins/SimulationView/layers_shadow.shader @@ -48,8 +48,10 @@ fragment = void main() { - if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5)) - { // actually, 8 and 9 + // travel moves: 8, 9, 12, 13 + if ((u_show_travel_moves == 0) && (((v_line_type >= 7.5) && (v_line_type <= 9.5)) || + ((v_line_type >= 11.5) && (v_line_type <= 13.5)))) { + { // discard movements discard; } @@ -124,7 +126,9 @@ fragment41core = void main() { - if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5)) { // actually, 8 and 9 + // travel moves: 8, 9, 12, 13 + if ((u_show_travel_moves == 0) && (((v_line_type >= 7.5) && (v_line_type <= 9.5)) || + ((v_line_type >= 11.5) && (v_line_type <= 13.5)))) { // discard movements discard; } diff --git a/plugins/SolidView/SolidView.py b/plugins/SolidView/SolidView.py index 7f32b0df7f..e25273cb13 100644 --- a/plugins/SolidView/SolidView.py +++ b/plugins/SolidView/SolidView.py @@ -1,13 +1,12 @@ # Copyright (c) 2021 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -import os.path from UM.View.View import View from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Selection import Selection from UM.Resources import Resources -from PyQt6.QtGui import QOpenGLContext, QDesktopServices, QImage -from PyQt6.QtCore import QSize, QUrl +from PyQt6.QtGui import QDesktopServices, QImage +from PyQt6.QtCore import QUrl import numpy as np import time @@ -36,11 +35,12 @@ class SolidView(View): """Standard view for mesh models.""" _show_xray_warning_preference = "view/show_xray_warning" + _show_overhang_preference = "view/show_overhang" def __init__(self): super().__init__() application = Application.getInstance() - application.getPreferences().addPreference("view/show_overhang", True) + application.getPreferences().addPreference(self._show_overhang_preference, True) application.globalContainerStackChanged.connect(self._onGlobalContainerChanged) self._enabled_shader = None self._disabled_shader = None @@ -212,7 +212,7 @@ class SolidView(View): global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack: - if Application.getInstance().getPreferences().getValue("view/show_overhang"): + if Application.getInstance().getPreferences().getValue(self._show_overhang_preference): # Make sure the overhang angle is valid before passing it to the shader if self._support_angle >= 0 and self._support_angle <= 90: self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(90 - self._support_angle))) @@ -289,8 +289,9 @@ class SolidView(View): def endRendering(self): # check whether the xray overlay is showing badness - if time.time() > self._next_xray_checking_time\ - and Application.getInstance().getPreferences().getValue(self._show_xray_warning_preference): + if (time.time() > self._next_xray_checking_time + and Application.getInstance().getPreferences().getValue(self._show_xray_warning_preference) + and self._xray_pass is not None): self._next_xray_checking_time = time.time() + self._xray_checking_update_time xray_img = self._xray_pass.getOutput() diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 0a714396aa..afdad6a4d0 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -1,6 +1,8 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import Optional + from PyQt6.QtCore import Qt, QTimer from PyQt6.QtWidgets import QApplication @@ -35,6 +37,7 @@ class SupportEraser(Tool): self._controller = self.getController() self._selection_pass = None + self._picking_pass: Optional[PickingPass] = None CuraApplication.getInstance().globalContainerStackChanged.connect(self._updateEnabled) # Note: if the selection is cleared with this tool active, there is no way to switch to @@ -84,12 +87,13 @@ class SupportEraser(Tool): # Only "normal" meshes can have anti_overhang_meshes added to them return - # Create a pass for picking a world-space location from the mouse location - active_camera = self._controller.getScene().getActiveCamera() - picking_pass = PickingPass(active_camera.getViewportWidth(), active_camera.getViewportHeight()) - picking_pass.render() + # Get the pass for picking a world-space location from the mouse location + if self._picking_pass is None: + self._picking_pass = Application.getInstance().getRenderer().getRenderPass("picking_selected") + if not self._picking_pass: + return - picked_position = picking_pass.getPickedPosition(event.x, event.y) + picked_position = self._picking_pass.getPickedPosition(event.x, event.y) # Add the anti_overhang_mesh cube at the picked location self._createEraserMesh(picked_node, picked_position) @@ -189,3 +193,6 @@ class SupportEraser(Tool): mesh.calculateNormals() return mesh + + def getRequiredExtraRenderingPasses(self) -> list[str]: + return ["picking_selected"] \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py index 3c8e53b2e9..0831ceebd3 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py @@ -163,7 +163,7 @@ class CloudApiClient: scope=self._scope, data=b"", callback=self._parseCallback(on_finished, CloudPrintResponse), - error_callback=on_error, + error_callback=self._parseError(on_error), timeout=self.DEFAULT_REQUEST_TIMEOUT) def doPrintJobAction(self, cluster_id: str, cluster_job_id: str, action: str, @@ -256,7 +256,6 @@ class CloudApiClient: """Creates a callback function so that it includes the parsing of the response into the correct model. The callback is added to the 'finished' signal of the reply. - :param reply: The reply that should be listened to. :param on_finished: The callback in case the response is successful. Depending on the endpoint it will be either a list or a single item. :param model: The type of the model to convert the response to. @@ -281,6 +280,25 @@ class CloudApiClient: self._anti_gc_callbacks.append(parse) return parse + def _parseError(self, + on_error: Callable[[CloudError, "QNetworkReply.NetworkError", int], None]) -> Callable[[QNetworkReply, "QNetworkReply.NetworkError"], None]: + + """Creates a callback function so that it includes the parsing of an explicit error response into the correct model. + + :param on_error: The callback in case the response gives an explicit error + """ + + def parse(reply: QNetworkReply, error: "QNetworkReply.NetworkError") -> None: + + self._anti_gc_callbacks.remove(parse) + + http_code, response = self._parseReply(reply) + result = CloudError(**response["errors"][0]) + on_error(result, error, http_code) + + self._anti_gc_callbacks.append(parse) + return parse + @classmethod def getMachineIDMap(cls) -> Dict[str, str]: if cls._machine_id_to_name is None: diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index 090355a3c0..010ef93fbd 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -27,9 +27,11 @@ from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOut from ..Messages.PrintJobUploadBlockedMessage import PrintJobUploadBlockedMessage from ..Messages.PrintJobUploadErrorMessage import PrintJobUploadErrorMessage from ..Messages.PrintJobUploadQueueFullMessage import PrintJobUploadQueueFullMessage +from ..Messages.PrintJobUploadPrinterInactiveMessage import PrintJobUploadPrinterInactiveMessage from ..Messages.PrintJobUploadSuccessMessage import PrintJobUploadSuccessMessage from ..Models.Http.CloudClusterResponse import CloudClusterResponse from ..Models.Http.CloudClusterStatus import CloudClusterStatus +from ..Models.Http.CloudError import CloudError from ..Models.Http.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest from ..Models.Http.CloudPrintResponse import CloudPrintResponse from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse @@ -87,7 +89,8 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): address="", connection_type=ConnectionType.CloudConnection, properties=properties, - parent=parent + parent=parent, + active=cluster.display_status != "inactive" ) self._api = api_client @@ -190,6 +193,8 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._received_print_jobs = status.print_jobs self._updatePrintJobs(status.print_jobs) + self._setActive(status.active) + def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, filter_by_machine: bool = False, **kwargs) -> None: @@ -291,19 +296,21 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self.writeFinished.emit() - def _onPrintUploadSpecificError(self, reply: "QNetworkReply", _: "QNetworkReply.NetworkError"): + def _onPrintUploadSpecificError(self, error: CloudError, _: "QNetworkReply.NetworkError", http_error: int): """ Displays a message when an error occurs specific to uploading print job (i.e. queue is full). """ - error_code = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) - if error_code == 409: - PrintJobUploadQueueFullMessage().show() + if http_error == 409: + if error.code == "printerInactive": + PrintJobUploadPrinterInactiveMessage().show() + else: + PrintJobUploadQueueFullMessage().show() else: PrintJobUploadErrorMessage(I18N_CATALOG.i18nc("@error:send", "Unknown error code when uploading print job: {0}", - error_code)).show() + http_error)).show() - Logger.log("w", "Upload of print job failed specifically with error code {}".format(error_code)) + Logger.log("w", "Upload of print job failed specifically with error code {}".format(http_error)) self._progress.hide() self._pre_upload_print_job = None diff --git a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadPrinterInactiveMessage.py b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadPrinterInactiveMessage.py new file mode 100644 index 0000000000..324259eea4 --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadPrinterInactiveMessage.py @@ -0,0 +1,20 @@ +# Copyright (c) 2020 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from UM import i18nCatalog +from UM.Message import Message + + +I18N_CATALOG = i18nCatalog("cura") + + +class PrintJobUploadPrinterInactiveMessage(Message): + """Message shown when uploading a print job to a cluster and the printer is inactive.""" + + def __init__(self) -> None: + super().__init__( + text = I18N_CATALOG.i18nc("@info:status", "The printer is inactive and cannot accept a new print job."), + title = I18N_CATALOG.i18nc("@info:title", "Printer inactive"), + lifetime = 10, + message_type=Message.MessageType.ERROR + ) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py index 713582b8ad..a1f22f7b36 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py @@ -10,7 +10,7 @@ class CloudClusterResponse(BaseModel): """Class representing a cloud connected cluster.""" def __init__(self, cluster_id: str, host_guid: str, host_name: str, is_online: bool, status: str, - host_internal_ip: Optional[str] = None, host_version: Optional[str] = None, + display_status: str, host_internal_ip: Optional[str] = None, host_version: Optional[str] = None, friendly_name: Optional[str] = None, printer_type: str = "ultimaker3", printer_count: int = 1, capabilities: Optional[List[str]] = None, **kwargs) -> None: """Creates a new cluster response object. @@ -20,6 +20,7 @@ class CloudClusterResponse(BaseModel): :param host_name: The name of the printer as configured during the Wi-Fi setup. Used as identifier for end users. :param is_online: Whether this cluster is currently connected to the cloud. :param status: The status of the cluster authentication (active or inactive). + :param display_status: The display status of the cluster. :param host_version: The firmware version of the cluster host. This is where the Stardust client is running on. :param host_internal_ip: The internal IP address of the host printer. :param friendly_name: The human readable name of the host printer. @@ -31,6 +32,7 @@ class CloudClusterResponse(BaseModel): self.host_guid = host_guid self.host_name = host_name self.status = status + self.display_status = display_status self.is_online = is_online self.host_version = host_version self.host_internal_ip = host_internal_ip @@ -51,5 +53,5 @@ class CloudClusterResponse(BaseModel): Convenience function for printing when debugging. :return: A human-readable representation of the data in this object. """ - return str({k: v for k, v in self.__dict__.items() if k in {"cluster_id", "host_guid", "host_name", "status", "is_online", "host_version", "host_internal_ip", "friendly_name", "printer_type", "printer_count", "capabilities"}}) + return str({k: v for k, v in self.__dict__.items() if k in {"cluster_id", "host_guid", "host_name", "status", "display_status", "is_online", "host_version", "host_internal_ip", "friendly_name", "printer_type", "printer_count", "capabilities"}}) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py index 5cd151d8ef..34249dc67a 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py @@ -14,6 +14,7 @@ class CloudClusterStatus(BaseModel): def __init__(self, printers: List[Union[ClusterPrinterStatus, Dict[str, Any]]], print_jobs: List[Union[ClusterPrintJobStatus, Dict[str, Any]]], generated_time: Union[str, datetime], + unavailable: bool = False, **kwargs) -> None: """Creates a new cluster status model object. @@ -23,6 +24,7 @@ class CloudClusterStatus(BaseModel): """ self.generated_time = self.parseDate(generated_time) + self.active = not unavailable self.printers = self.parseModels(ClusterPrinterStatus, printers) self.print_jobs = self.parseModels(ClusterPrintJobStatus, print_jobs) super().__init__(**kwargs) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py index 925b4844c1..260d276427 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py @@ -20,13 +20,23 @@ from ..BaseModel import BaseModel class ClusterPrinterStatus(BaseModel): """Class representing a cluster printer""" - def __init__(self, enabled: bool, firmware_version: str, friendly_name: str, ip_address: str, machine_variant: str, - status: str, unique_name: str, uuid: str, - configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]], - reserved_by: Optional[str] = None, maintenance_required: Optional[bool] = None, - firmware_update_status: Optional[str] = None, latest_available_firmware: Optional[str] = None, - build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None, - material_station: Union[Dict[str, Any], ClusterPrinterMaterialStation] = None, **kwargs) -> None: + def __init__(self, + enabled: Optional[bool] = True, + friendly_name: Optional[str] = "", + machine_variant: Optional[str] = "", + status: Optional[str] = "unknown", + unique_name: Optional[str] = "", + uuid: Optional[str] = "", + configuration: Optional[List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]]] = None, + firmware_version: Optional[str] = None, + ip_address: Optional[str] = None, + reserved_by: Optional[str] = "", + maintenance_required: Optional[bool] = False, + firmware_update_status: Optional[str] = "", + latest_available_firmware: Optional[str] = "", + build_plate: Optional[Union[Dict[str, Any], ClusterBuildPlate]] = None, + material_station: Optional[Union[Dict[str, Any], ClusterPrinterMaterialStation]] = None, + **kwargs) -> None: """ Creates a new cluster printer status :param enabled: A printer can be disabled if it should not receive new jobs. By default, every printer is enabled. @@ -47,7 +57,7 @@ class ClusterPrinterStatus(BaseModel): :param material_station: The material station that is on the printer. """ - self.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration) + self.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration) if configuration else [] self.enabled = enabled self.firmware_version = firmware_version self.friendly_name = friendly_name @@ -70,7 +80,7 @@ class ClusterPrinterStatus(BaseModel): :param controller: - The controller of the model. """ - model = PrinterOutputModel(controller, len(self.configuration), firmware_version = self.firmware_version) + model = PrinterOutputModel(controller, len(self.configuration), firmware_version = self.firmware_version or "") self.updateOutputModel(model) return model @@ -86,7 +96,8 @@ class ClusterPrinterStatus(BaseModel): model.updateType(self.machine_variant) model.updateState(self.status if self.enabled else "disabled") model.updateBuildplate(self.build_plate.type if self.build_plate else "glass") - model.setCameraUrl(QUrl("http://{}:8080/?action=stream".format(self.ip_address))) + if self.ip_address: + model.setCameraUrl(QUrl("http://{}:8080/?action=stream".format(self.ip_address))) if not model.printerConfiguration: # Prevent accessing printer configuration when not available. diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index 8f25df37db..3ac5ccc7e7 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -46,10 +46,10 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): QUEUED_PRINT_JOBS_STATES = {"queued", "error"} def __init__(self, device_id: str, address: str, properties: Dict[bytes, bytes], connection_type: ConnectionType, - parent=None) -> None: + parent=None, active: bool = True) -> None: super().__init__(device_id=device_id, address=address, properties=properties, connection_type=connection_type, - parent=parent) + parent=parent, active=active) # Trigger the printersChanged signal when the private signal is triggered. self.printersChanged.connect(self._clusterPrintersChanged) diff --git a/resources/definitions/anycubic_kobra3v2.def.json b/resources/definitions/anycubic_kobra3v2.def.json new file mode 100644 index 0000000000..6b8df0cc4b --- /dev/null +++ b/resources/definitions/anycubic_kobra3v2.def.json @@ -0,0 +1,52 @@ +{ + "version": 2, + "name": "Anycubic Kobra 3 v2", + "inherits": "fdmprinter", + "metadata": + { + "visible": true, + "author": "Sam Bonnekamp", + "manufacturer": "Anycubic", + "file_formats": "text/x-gcode", + "platform": "anycubic_kobra3v2_buildplate.stl", + "has_textured_buildplate": true, + "machine_extruder_trains": { "0": "anycubic_kobra3v2_extruder_0" } + }, + "overrides": + { + "adhesion_type": { "value": "'skirt'" }, + "layer_height": { "default_value": 0.2 }, + "machine_buildplate_type": { "default_value": "PEI Spring Steel" }, + "machine_center_is_zero": { "default_value": false }, + "machine_depth": { "default_value": 250 }, + "machine_end_gcode": { "default_value": "G1 X5 Y{machine_depth*0.95} F{speed_travel*60} ; present print\nM140 S0 ; turn off heat bed\nM104 S0 ; turn off temperature\nM84; disable motors ; disable stepper motors" }, + "machine_heated_bed": { "default_value": true }, + "machine_height": { "default_value": 260 }, + "machine_name": + { + "default_value": "Anycubic Kobra 3 v2", + "description": "Anycubic Kobra 3 v2" + }, + "machine_start_gcode": { "default_value": "; thumbnail begin 32x32\n; iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAX\n; NSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAARWSURBVHgB1Vc7bx1FFD5n/cC+YOQ4RBTXlg0STfIH\n; gM78h0SAhGgQjyqARO8eFEQFBaKhCHIoKagcOkJJQUWBI19XJLaJgp+5M8x5zZzdO+sg0ZCR1zM7u3\n; e+73znMbMAT0KLMVqPdKVhvuze+o2NjUafget7Gz4GeO3s7Ow29YiYiXSJ2TPft0BwEmZ2dpYnp+Ec\n; 8NPT09tpyOAhhF4CXSBPwhOvEZk+B/ynNFzd2QvwxlfHMEq9PqR//mVaWcegmqKCIf9FKGS2Px30Ey\n; A/Hh8fE/gWgRPomww+VkxVgYFoWcz3xXpUJo2QsXvEFu8qAQJvmobA13b3I1u+Q+AhQuRfB1kTBDS6\n; FUVmVBx5iwmqNIgNv9ONkUzg4ODgBQNn2b80yyNbCzFQqMsC4MCNg/eveSQGdQM6glhXYDAYbJHvRw\n; q+ux90dQ8eeBxacaD+5T5ZSdLbfIcT61hTgCZTuq1R/zqDjxMOva6SKzBdMQRoO1OsovcLZrpPkmOO\n; ytibCdPejOQCGJHl5l+zlEGNxBiuvjxI1zNwZTgDzw4a6LbV6/fTT8eqSGD/G3hVgTQRT05OOv6Nyl\n; zBwxgW5iN8/e4leOWlOTivESjAlNhvYJoEvTEA6gq7wIqMyZ76zevPw5XlWdi5/wi+2foLNu88hAeH\n; QQKQJG/ScjjFY1cUFD3mYKxmgeef+1z9AstO4KMEfu3zXdi594gXlHIj3hYFY6su5fVQU7OjgAvZNg\n; kpaDFn8jvrC9zf+GEvgZ+16HqX5QRVctl6FmAyXhq3TmHnROBhWuBysp7aj7/+rS+IWybKO8oPWTns\n; zFX2kxalEKTe+6pZXR9kx0X3olhXdmqJ+GIF6lyfC8ArUCnZ8PPvJ9xfXn4KLMCkuBlo306payJMWD\n; 9BoGspE9LrVop4ajfevgQrz82U5xkQyg7onyEWfpUzxSQBlUnjWRZM8t66cwi/jU5h5eIMbH40hKuv\n; LqS6UFwh7mhv1RL4AYojY9VQbqkQRWL34icPtQ5o6dXqR9fwQlLgrYuPLUSrH96TvZDigsg1UwxFc3\n; c/e5rfsRNRI/JFNNns8MBy8QKNvJYKzGgf4NoXf8LH3+5xTDw4jP0szH16ZvBlybdcitMhpJzrwKUB\n; jcmCiDkDvv/liK92qRWLeTdsZYSsQaPhhXOOZD44uLJhqXAIxYKIQUIaSqBGg+Di09CuBiUYxQ0E/t\n; 0Hc2bk9gQB26kiYJEK7a6kJyqdvL/bKSiKOmx9dGumfpnA35/jnsDH4/H6BAH7Qba3dobTTQYrOY0N\n; usOnbr9JieEiCPgSzzH4/Pz8dpVASw3T2OY0Y2V7FkBrdjDOarErkLPmpgNPkb/u5e8l4CqHG1IqhU\n; xEoQuZztlwuIRw8705WFma8uB3a0jcUhb05pQ/zdp+Ufso8R8hdLqygDs6OnptcXHxD+gx9V8R6H6a\n; dQ8WrSwq81XZ/1NToLztuY9T8D3U687/r/0D2siIlZoKRzIAAAAASUVORK5CYII=\n; thumbnail end\n; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\n;TYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" }, + "machine_start_gcode_first": { "default_value": true }, + "machine_width": { "default_value": 250 }, + "material_bed_temperature": + { + "maximum_value": "110", + "maximum_value_warning": "90" + }, + "material_diameter": { "default_value": 1.75 }, + "material_initial_print_temperature": + { + "maximum_value_warning": 295, + "value": "material_print_temperature + 5" + }, + "material_print_temperature": { "maximum_value_warning": 250 }, + "material_print_temperature_layer_0": + { + "maximum_value_warning": 295, + "value": "material_print_temperature + 5" + }, + "relative_extrusion": { "value": true } + } +} \ No newline at end of file diff --git a/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json b/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json new file mode 100644 index 0000000000..fc464c9eee --- /dev/null +++ b/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json @@ -0,0 +1,61 @@ +{ + "version": 2, + "name": "Anycubic Kobra 3 v2 ACE PRO", + "inherits": "fdmprinter", + "metadata": + { + "visible": true, + "author": "Sam Bonnekamp", + "manufacturer": "Anycubic", + "file_formats": "text/x-gcode", + "platform": "anycubic_kobra3v2_buildplate.stl", + "has_textured_buildplate": true, + "machine_extruder_trains": + { + "0": "anycubic_kobra3v2_ACEPRO_extruder_0", + "1": "anycubic_kobra3v2_ACEPRO_extruder_1", + "2": "anycubic_kobra3v2_ACEPRO_extruder_2", + "3": "anycubic_kobra3v2_ACEPRO_extruder_3" + } + }, + "overrides": + { + "adhesion_type": { "value": "'skirt'" }, + "layer_height": { "default_value": 0.2 }, + "machine_buildplate_type": { "default_value": "PEI Spring Steel" }, + "machine_center_is_zero": { "default_value": false }, + "machine_depth": { "default_value": 250 }, + "machine_end_gcode": { "default_value": "G1 X5 Y{machine_depth*0.95} F{speed_travel*60} ; present print\nM140 S0 ; turn off heat bed\nM104 S0 ; turn off temperature\nM84; disable motors ; disable stepper motors" }, + "machine_extruder_count": { "default_value": 4 }, + "machine_heated_bed": { "default_value": true }, + "machine_height": { "default_value": 260 }, + "machine_name": + { + "default_value": "Anycubic Kobra 3 v2", + "description": "Anycubic Kobra 3 v2" + }, + "machine_start_gcode": { "default_value": "; thumbnail begin 32x32\n; iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAX\n; NSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAARWSURBVHgB1Vc7bx1FFD5n/cC+YOQ4RBTXlg0STfIH\n; gM78h0SAhGgQjyqARO8eFEQFBaKhCHIoKagcOkJJQUWBI19XJLaJgp+5M8x5zZzdO+sg0ZCR1zM7u3\n; e+73znMbMAT0KLMVqPdKVhvuze+o2NjUafget7Gz4GeO3s7Ow29YiYiXSJ2TPft0BwEmZ2dpYnp+Ec\n; 8NPT09tpyOAhhF4CXSBPwhOvEZk+B/ynNFzd2QvwxlfHMEq9PqR//mVaWcegmqKCIf9FKGS2Px30Ey\n; A/Hh8fE/gWgRPomww+VkxVgYFoWcz3xXpUJo2QsXvEFu8qAQJvmobA13b3I1u+Q+AhQuRfB1kTBDS6\n; FUVmVBx5iwmqNIgNv9ONkUzg4ODgBQNn2b80yyNbCzFQqMsC4MCNg/eveSQGdQM6glhXYDAYbJHvRw\n; q+ux90dQ8eeBxacaD+5T5ZSdLbfIcT61hTgCZTuq1R/zqDjxMOva6SKzBdMQRoO1OsovcLZrpPkmOO\n; ytibCdPejOQCGJHl5l+zlEGNxBiuvjxI1zNwZTgDzw4a6LbV6/fTT8eqSGD/G3hVgTQRT05OOv6Nyl\n; zBwxgW5iN8/e4leOWlOTivESjAlNhvYJoEvTEA6gq7wIqMyZ76zevPw5XlWdi5/wi+2foLNu88hAeH\n; QQKQJG/ScjjFY1cUFD3mYKxmgeef+1z9AstO4KMEfu3zXdi594gXlHIj3hYFY6su5fVQU7OjgAvZNg\n; kpaDFn8jvrC9zf+GEvgZ+16HqX5QRVctl6FmAyXhq3TmHnROBhWuBysp7aj7/+rS+IWybKO8oPWTns\n; zFX2kxalEKTe+6pZXR9kx0X3olhXdmqJ+GIF6lyfC8ArUCnZ8PPvJ9xfXn4KLMCkuBlo306payJMWD\n; 9BoGspE9LrVop4ajfevgQrz82U5xkQyg7onyEWfpUzxSQBlUnjWRZM8t66cwi/jU5h5eIMbH40hKuv\n; LqS6UFwh7mhv1RL4AYojY9VQbqkQRWL34icPtQ5o6dXqR9fwQlLgrYuPLUSrH96TvZDigsg1UwxFc3\n; c/e5rfsRNRI/JFNNns8MBy8QKNvJYKzGgf4NoXf8LH3+5xTDw4jP0szH16ZvBlybdcitMhpJzrwKUB\n; jcmCiDkDvv/liK92qRWLeTdsZYSsQaPhhXOOZD44uLJhqXAIxYKIQUIaSqBGg+Di09CuBiUYxQ0E/t\n; 0Hc2bk9gQB26kiYJEK7a6kJyqdvL/bKSiKOmx9dGumfpnA35/jnsDH4/H6BAH7Qba3dobTTQYrOY0N\n; usOnbr9JieEiCPgSzzH4/Pz8dpVASw3T2OY0Y2V7FkBrdjDOarErkLPmpgNPkb/u5e8l4CqHG1IqhU\n; xEoQuZztlwuIRw8705WFma8uB3a0jcUhb05pQ/zdp+Ufso8R8hdLqygDs6OnptcXHxD+gx9V8R6H6a\n; dQ8WrSwq81XZ/1NToLztuY9T8D3U687/r/0D2siIlZoKRzIAAAAASUVORK5CYII=\n; thumbnail end\n; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\n;TYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" }, + "machine_start_gcode_first": { "default_value": true }, + "machine_width": { "default_value": 250 }, + "material_bed_temperature": + { + "maximum_value": "110", + "maximum_value_warning": "90" + }, + "material_diameter": { "default_value": 1.75 }, + "material_initial_print_temperature": + { + "maximum_value_warning": 295, + "value": "material_print_temperature + 5" + }, + "material_print_temp_wait": { "value": true }, + "material_print_temperature": { "maximum_value": 300 }, + "material_print_temperature_layer_0": + { + "maximum_value_warning": 295, + "value": "material_print_temperature + 5" + }, + "material_standby_temperature": { "default_value": "material_print_temperature" }, + "relative_extrusion": { "value": true } + } +} \ No newline at end of file diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 6710d0f0bf..68852d805f 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -1681,7 +1681,7 @@ "maximum_value": "999999", "type": "int", "minimum_value_warning": "2", - "value": "0 if infill_sparse_density == 100 else math.ceil(round(top_thickness / resolveOrValue('layer_height'), 4))", + "value": "math.ceil(round(top_thickness / resolveOrValue('layer_height'), 4))", "limit_to_extruder": "top_bottom_extruder_nr", "settable_per_mesh": true } @@ -1711,7 +1711,7 @@ "default_value": 6, "maximum_value": "999999", "type": "int", - "value": "999999 if infill_sparse_density == 100 and not magic_spiralize else math.ceil(round(bottom_thickness / resolveOrValue('layer_height'), 4))", + "value": "math.ceil(round(bottom_thickness / resolveOrValue('layer_height'), 4))", "limit_to_extruder": "top_bottom_extruder_nr", "settable_per_mesh": true }, @@ -4568,7 +4568,7 @@ "minimum_value_warning": "-0.0001", "maximum_value_warning": "10.0", "enabled": "retraction_enable and machine_gcode_flavor != \"UltiGCode\"", - "settable_per_mesh": false, + "settable_per_mesh": true, "settable_per_extruder": true }, "retraction_speed": @@ -4658,7 +4658,7 @@ "maximum_value": 999999999, "type": "int", "enabled": "retraction_enable", - "settable_per_mesh": false, + "settable_per_mesh": true, "settable_per_extruder": true }, "retraction_extrusion_window": @@ -4672,7 +4672,7 @@ "maximum_value_warning": "retraction_amount * 2", "value": "retraction_amount", "enabled": "retraction_enable", - "settable_per_mesh": false, + "settable_per_mesh": true, "settable_per_extruder": true }, "retraction_combing": @@ -9311,6 +9311,42 @@ "default_value": true, "settable_per_mesh": true }, + "retraction_during_travel_ratio": + { + "label": "Retraction During Travel Move", + "description": "The ratio of retraction performed during the travel move, with the remainder completed while the nozzle is stationary, before traveling", + "unit": "%", + "type": "float", + "default_value": 0, + "minimum_value": 0, + "maximum_value": 100, + "enabled": "retraction_enable and not machine_firmware_retract and machine_gcode_flavor != \"UltiGCode\" and machine_gcode_flavor != \"BFB\"", + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "keep_retracting_during_travel": + { + "label": "Keep Retracting During Travel", + "description": "When retraction during travel is enabled, and there is more than enough time to perform a full retract during a travel move, spread the retraction over the whole travel move with a lower retraction speed, so that we do not travel with a non-retracting nozzle. This can help reducing oozing.", + "type": "bool", + "default_value": false, + "enabled": "retraction_enable and not machine_firmware_retract and machine_gcode_flavor != \"UltiGCode\" and machine_gcode_flavor != \"BFB\" and retraction_during_travel_ratio > 0", + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "prime_during_travel_ratio": + { + "label": "Prime During Travel Move", + "description": "The ratio of priming performed during the travel move, with the remainder completed while the nozzle is stationary, after traveling", + "unit": "%", + "type": "float", + "default_value": 0, + "minimum_value": 0, + "maximum_value": 100, + "enabled": "retraction_enable and not machine_firmware_retract and machine_gcode_flavor != \"UltiGCode\" and machine_gcode_flavor != \"BFB\"", + "settable_per_mesh": false, + "settable_per_extruder": true + }, "scarf_joint_seam_length": { "label": "Scarf Seam Length", diff --git a/resources/definitions/sovol_sv08.def.json b/resources/definitions/sovol_sv08.def.json new file mode 100644 index 0000000000..a662b19618 --- /dev/null +++ b/resources/definitions/sovol_sv08.def.json @@ -0,0 +1,130 @@ +{ + "version": 2, + "name": "Sovol SV08", + "inherits": "fdmprinter", + "metadata": + { + "visible": true, + "author": "Steinar H. Gunderson", + "manufacturer": "Sovol 3D", + "file_formats": "text/x-gcode", + "platform": "sovol_sv08_buildplate_model.stl", + "exclude_materials": [], + "first_start_actions": [ "MachineSettingsAction" ], + "has_machine_quality": true, + "has_materials": true, + "has_variants": true, + "machine_extruder_trains": { "0": "sovol_sv08_extruder" }, + "preferred_material": "generic_abs", + "preferred_quality_type": "fast", + "preferred_variant_name": "0.4mm Nozzle", + "quality_definition": "sovol_sv08", + "variants_name": "Nozzle Size" + }, + "overrides": + { + "acceleration_enabled": { "default_value": false }, + "acceleration_layer_0": { "value": 1800 }, + "acceleration_print": { "default_value": 2200 }, + "acceleration_roofing": { "value": 1800 }, + "acceleration_travel_layer_0": { "value": 1800 }, + "acceleration_wall_0": { "value": 1800 }, + "adhesion_type": { "default_value": "skirt" }, + "alternate_extra_perimeter": { "default_value": true }, + "bridge_fan_speed": { "default_value": 100 }, + "bridge_fan_speed_2": { "resolve": "max(cool_fan_speed, 50)" }, + "bridge_fan_speed_3": { "resolve": "max(cool_fan_speed, 20)" }, + "bridge_settings_enabled": { "default_value": true }, + "bridge_wall_coast": { "default_value": 10 }, + "cool_fan_full_at_height": { "value": "resolveOrValue('layer_height_0') + resolveOrValue('layer_height') * max(1, cool_fan_full_layer - 1)" }, + "cool_fan_full_layer": { "value": 4 }, + "cool_fan_speed_min": { "value": "cool_fan_speed" }, + "cool_min_layer_time": { "default_value": 15 }, + "cool_min_layer_time_fan_speed_max": { "default_value": 20 }, + "fill_outline_gaps": { "default_value": true }, + "gantry_height": { "value": 30 }, + "infill_before_walls": { "default_value": false }, + "infill_enable_travel_optimization": { "default_value": true }, + "jerk_enabled": { "default_value": false }, + "jerk_roofing": { "value": 10 }, + "jerk_wall_0": { "value": 10 }, + "layer_height_0": { "resolve": "max(0.2, min(extruderValues('layer_height')))" }, + "line_width": { "value": "machine_nozzle_size * 1.125" }, + "machine_acceleration": { "default_value": 1500 }, + "machine_depth": { "default_value": 350 }, + "machine_end_gcode": { "default_value": "END_PRINT\n" }, + "machine_endstop_positive_direction_x": { "default_value": true }, + "machine_endstop_positive_direction_y": { "default_value": true }, + "machine_endstop_positive_direction_z": { "default_value": false }, + "machine_feeder_wheel_diameter": { "default_value": 7.5 }, + "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, + "machine_head_with_fans_polygon": + { + "default_value": [ + [-35, 65], + [-35, -50], + [35, -50], + [35, 65] + ] + }, + "machine_heated_bed": { "default_value": true }, + "machine_height": { "default_value": 345 }, + "machine_max_acceleration_e": { "default_value": 5000 }, + "machine_max_acceleration_x": { "default_value": 40000 }, + "machine_max_acceleration_y": { "default_value": 40000 }, + "machine_max_acceleration_z": { "default_value": 500 }, + "machine_max_feedrate_e": { "default_value": 50 }, + "machine_max_feedrate_x": { "default_value": 700 }, + "machine_max_feedrate_y": { "default_value": 700 }, + "machine_max_feedrate_z": { "default_value": 20 }, + "machine_max_jerk_e": { "default_value": 5 }, + "machine_max_jerk_xy": { "default_value": 20 }, + "machine_max_jerk_z": { "default_value": 0.5 }, + "machine_name": { "default_value": "SV08" }, + "machine_start_gcode": { "default_value": "G28 ; Move to zero\nG90 ; Absolute positioning\nG1 X0 F9000\nG1 Y20\nG1 Z0.600 F600\nG1 Y0 F9000\nSTART_PRINT EXTRUDER_TEMP={material_print_temperature_layer_0} BED_TEMP={material_bed_temperature_layer_0}\nG90 ; Absolute positioning (START_PRINT might have changed it)\nG1 X0 F9000\nG1 Y20\nG1 Z0.600 F600\nG1 Y0 F9000\nM400\nG91 ; Relative positioning\nM83 ; Relative extrusion\nM140 S{material_bed_temperature_layer_0} ; Set bed temp\nM104 S{material_print_temperature_layer_0} ; Set extruder temp\nM190 S{material_bed_temperature_layer_0} ; Wait for bed temp\nM109 S{material_print_temperature_layer_0} ; Wait for extruder temp\n{if machine_nozzle_size >= 0.4}\n; Standard Sovol blob and purge line.\nG1 E25 F300 ; Purge blob\nG4 P1000 ; Wait a bit to let it finish\nG1 E-0.200 Z5 F600 ; Retract\nG1 X88.000 F9000 ; Move right and then down for purge line\nG1 Z-5.000 F600\nG1 X87.000 E20.88 F1800 ; Purge line right\nG1 X87.000 E13.92 F1800\nG1 Y1 E0.16 F1800 ; Small movement back for next line\nG1 X-87.000 E13.92 F1800 ; Purge line left\nG1 X-87.000 E20.88 F1800\nG1 Y1 E0.24 F1800 ; Small movement back for next line\nG1 X87.000 E20.88 F1800 ; Purge line right\nG1 X87.000 E13.92 F1800\nG1 E-0.200 Z1 F600\n{else}\n; The default start G-code uses too high flow for smaller nozzles,\n; which causes Klipper errors. Scale everything back by\n; (0.25/0.4)^2, i.e., for 0.25mm nozzle. This should be good\n; enough for 0.2mm as well.\nG1 E8 F300 ; Purge blob\nG4 P1000 ; Wait a bit to let it finish\nG1 E-0.078 Z5 F600 ; Retract\nG1 X88.000 F9000 ; Move right and then down for purge line\nG1 Z-5.000 F600\nG1 X87.000 E8.16 F1800 ; Purge line right\nG1 X87.000 E5.44 F1800\nG1 Y1 E0.063 F1800 ; Small movement back for next line\nG1 X-87.000 E5.44 F1800 ; Purge line left\nG1 X-87.000 E8.16 F1800\nG1 Y1 E0.094 F1800 ; Small movement back for next line\nG1 X87.000 E8.16 F1800 ; Purge line right\nG1 X87.000 E5.44 F1800\nG1 E-0.078 Z1 F600\n{endif}\nM400 ; Wait for moves to finish\nG90 ; Absolute positioning\nM82 ; Absolute extrusion mode\n" }, + "machine_steps_per_mm_x": { "default_value": 80 }, + "machine_steps_per_mm_y": { "default_value": 80 }, + "machine_steps_per_mm_z": { "default_value": 400 }, + "machine_width": { "default_value": 350 }, + "meshfix_maximum_resolution": { "default_value": 0.01 }, + "min_infill_area": { "default_value": 5.0 }, + "minimum_polygon_circumference": { "default_value": 0.2 }, + "optimize_wall_printing_order": { "default_value": true }, + "retraction_amount": { "default_value": 0.5 }, + "retraction_combing": { "value": "'noskin'" }, + "retraction_combing_max_distance": { "default_value": 10 }, + "retraction_hop": { "default_value": 0.4 }, + "retraction_hop_enabled": { "default_value": true }, + "retraction_prime_speed": + { + "maximum_value_warning": 130, + "value": "math.ceil(retraction_speed * 0.4)" + }, + "retraction_retract_speed": { "maximum_value_warning": 130 }, + "retraction_speed": + { + "default_value": 30, + "maximum_value_warning": 130 + }, + "roofing_layer_count": { "value": 1 }, + "skirt_brim_minimal_length": { "default_value": 550 }, + "speed_layer_0": { "value": "math.ceil(speed_print * 0.25)" }, + "speed_roofing": { "value": "math.ceil(speed_print * 0.33)" }, + "speed_slowdown_layers": { "default_value": 4 }, + "speed_topbottom": { "value": "math.ceil(speed_print * 0.33)" }, + "speed_travel": + { + "maximum_value_warning": 501, + "value": 300 + }, + "speed_travel_layer_0": { "value": "math.ceil(speed_travel * 0.4)" }, + "speed_wall": { "value": "math.ceil(speed_print * 0.33)" }, + "speed_wall_0": { "value": "math.ceil(speed_print * 0.33)" }, + "speed_wall_x": { "value": "math.ceil(speed_print * 0.66)" }, + "travel_avoid_other_parts": { "default_value": false }, + "wall_line_width": { "value": "machine_nozzle_size" }, + "wall_overhang_angle": { "default_value": 75 }, + "wall_overhang_speed_factors": { "default_value": "[50]" }, + "zig_zaggify_infill": { "value": true } + } +} \ No newline at end of file diff --git a/resources/definitions/ultimaker.def.json b/resources/definitions/ultimaker.def.json index d8fdd7eede..c5f441479b 100644 --- a/resources/definitions/ultimaker.def.json +++ b/resources/definitions/ultimaker.def.json @@ -16,7 +16,6 @@ { "acceleration_layer_0": { "value": "acceleration_topbottom" }, "acceleration_travel_enabled": { "value": false }, - "bottom_layers": { "value": "math.ceil(round(bottom_thickness / resolveOrValue('layer_height'), 4))" }, "bridge_enable_more_layers": { "value": false }, "bridge_fan_speed": { "value": "cool_fan_speed_max" }, "bridge_fan_speed_2": { "value": "cool_fan_speed_min" }, @@ -219,7 +218,6 @@ "support_wall_count": { "value": "1 if support_structure == 'tree' else 0" }, "support_xy_distance_overhang": { "value": "0.2" }, "support_z_distance": { "value": "0" }, - "top_layers": { "value": "math.ceil(round(top_thickness / resolveOrValue('layer_height'), 4))" }, "wall_0_material_flow_layer_0": { "value": "1.10 * material_flow_layer_0" }, "wall_thickness": { "value": "wall_line_width_0 + wall_line_width_x" }, "wall_x_material_flow_layer_0": { "value": "0.95 * material_flow_layer_0" }, diff --git a/resources/definitions/ultimaker_s8.def.json b/resources/definitions/ultimaker_s8.def.json index b7de27722d..cbd6e255f8 100644 --- a/resources/definitions/ultimaker_s8.def.json +++ b/resources/definitions/ultimaker_s8.def.json @@ -98,7 +98,7 @@ { "maximum_value": "machine_max_acceleration_x", "maximum_value_warning": "machine_max_acceleration_x*0.8", - "value": "acceleration_wall_0" + "value": "acceleration_topbottom / 2" }, "acceleration_skirt_brim": { @@ -199,15 +199,19 @@ }, "adhesion_type": { "value": "'brim' if support_enable and support_structure=='tree' else 'skirt'" }, "bottom_thickness": { "value": "3*layer_height if top_layers==4 and not support_enable else top_bottom_thickness" }, - "bridge_skin_material_flow": { "value": 200 }, + "bridge_enable_more_layers": { "value": true }, + "bridge_skin_density": { "value": 70 }, + "bridge_skin_material_flow": { "value": 150 }, + "bridge_skin_material_flow_2": { "value": 70 }, "bridge_skin_speed": { "unit": "mm/s", - "value": "bridge_wall_speed" + "value": 35 }, + "bridge_skin_speed_2": { "value": "speed_print*2/3" }, "bridge_sparse_infill_max_density": { "value": 50 }, - "bridge_wall_material_flow": { "value": "bridge_skin_material_flow" }, - "bridge_wall_min_length": { "value": 10 }, + "bridge_wall_material_flow": { "value": 200 }, + "bridge_wall_min_length": { "value": 2 }, "bridge_wall_speed": { "unit": "mm/s", @@ -221,13 +225,13 @@ ] }, "cool_during_extruder_switch": { "value": "'all_fans'" }, - "cool_min_layer_time": { "value": 5 }, - "cool_min_layer_time_overhang": { "value": 9 }, - "cool_min_layer_time_overhang_min_segment_length": { "value": 2 }, + "cool_min_layer_time": { "value": 6 }, + "cool_min_layer_time_overhang": { "value": 11 }, + "cool_min_layer_time_overhang_min_segment_length": { "value": 1.5 }, "cool_min_speed": { "value": 6 }, "cool_min_temperature": { - "minimum_value_warning": "material_print_temperature-15", + "minimum_value_warning": "material_print_temperature-20", "value": "material_print_temperature-15" }, "default_material_print_temperature": { "maximum_value_warning": 320 }, @@ -235,9 +239,9 @@ "flooring_layer_count": { "value": 1 }, "gradual_flow_enabled": { "value": false }, "hole_xy_offset": { "value": 0.075 }, - "infill_material_flow": { "value": "material_flow" }, + "infill_material_flow": { "value": "material_flow if infill_sparse_density < 95 else 95" }, "infill_overlap": { "value": 10 }, - "infill_pattern": { "value": "'zigzag' if infill_sparse_density > 80 else 'grid'" }, + "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 }, @@ -281,7 +285,7 @@ { "maximum_value_warning": "machine_max_jerk_xy / 2", "unit": "m/s\u00b3", - "value": "jerk_wall_0" + "value": "jerk_print" }, "jerk_skirt_brim": { @@ -438,10 +442,11 @@ "retraction_hop": { "value": 1 }, "retraction_hop_after_extruder_switch_height": { "value": 2 }, "retraction_hop_enabled": { "value": true }, - "retraction_min_travel": { "value": "5 if support_enable and support_structure=='tree' else line_width * 2.5" }, + "retraction_min_travel": { "value": "2.5 if support_enable and support_structure=='tree' else line_width * 2.5" }, "retraction_prime_speed": { "value": 15 }, "skin_edge_support_thickness": { "value": 0 }, - "skin_material_flow": { "value": 95 }, + "skin_material_flow": { "value": 93 }, + "skin_outline_count": { "value": 0 }, "skin_overlap": { "value": 0 }, "skin_preshrink": { "value": 0 }, "skirt_brim_minimal_length": { "value": 1000 }, @@ -571,38 +576,46 @@ "value": "speed_wall" }, "support_angle": { "value": 60 }, - "support_bottom_distance": { "maximum_value_warning": "3*layer_height" }, + "support_bottom_distance": + { + "maximum_value_warning": "3*layer_height", + "value": "support_z_distance" + }, "support_bottom_offset": { "value": 0 }, "support_brim_width": { "value": 10 }, "support_interface_enable": { "value": true }, "support_interface_offset": { "value": "support_offset" }, "support_line_width": { "value": "1.25*line_width" }, - "support_offset": { "value": "1.2 if support_structure == 'tree' else 0.8" }, + "support_offset": { "value": 0.8 }, "support_pattern": { "value": "'gyroid' if support_structure == 'tree' else 'lines'" }, "support_roof_height": { "minimum_value_warning": 0 }, "support_structure": { "value": "'normal'" }, "support_top_distance": { "maximum_value_warning": "3*layer_height" }, "support_tree_angle": { "value": 50 }, "support_tree_angle_slow": { "value": 35 }, - "support_tree_bp_diameter": { "value": 15 }, - "support_tree_branch_diameter": { "value": 8 }, - "support_tree_tip_diameter": { "value": 1.0 }, - "support_tree_top_rate": { "value": 20 }, - "support_xy_distance_overhang": { "value": "machine_nozzle_size" }, - "support_z_distance": { "value": "0.4*material_shrinkage_percentage_z/100.0" }, - "top_bottom_thickness": { "value": "round(4*layer_height, 2)" }, + "support_tree_bp_diameter": { "value": 20 }, + "support_tree_branch_diameter": { "value": 5 }, + "support_tree_branch_diameter_angle": { "value": 5 }, + "support_tree_max_diameter": { "value": 15 }, + "support_tree_tip_diameter": { "value": 2.0 }, + "support_tree_top_rate": { "value": 10 }, + "support_xy_distance": { "value": 1.2 }, + "support_xy_distance_overhang": { "value": "1.5*machine_nozzle_size" }, + "support_z_distance": { "value": "2*layer_height" }, + "top_bottom_thickness": { "value": "wall_thickness" }, "travel_avoid_other_parts": { "value": true }, "travel_avoid_supports": { "value": true }, "wall_0_acceleration": { "value": 1000 }, "wall_0_deceleration": { "value": 1000 }, "wall_0_end_speed_ratio": { "value": 100 }, + "wall_0_inset": { "value": 0.05 }, "wall_0_speed_split_distance": { "value": 0.2 }, "wall_0_start_speed_ratio": { "value": 100 }, "wall_0_wipe_dist": { "value": 0 }, "wall_material_flow": { "value": 95 }, "wall_overhang_angle": { "value": 45 }, "wall_x_material_flow": { "value": 100 }, - "xy_offset": { "value": 0.05 }, + "xy_offset": { "value": 0.075 }, "z_seam_corner": { "value": "'z_seam_corner_weighted'" }, "z_seam_position": { "value": "'backright'" }, "z_seam_type": { "value": "'sharpest_corner'" } diff --git a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_0.def.json b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_0.def.json new file mode 100644 index 0000000000..5537606b12 --- /dev/null +++ b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_0.def.json @@ -0,0 +1,16 @@ +{ + "version": 2, + "name": "ACE Pro Color 1", + "inherits": "fdmextruder", + "metadata": + { + "machine": "anycubic_kobra3v2_ACE_PRO", + "position": "0" + }, + "overrides": + { + "extruder_nr": { "default_value": 0 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} \ No newline at end of file diff --git a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_1.def.json b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_1.def.json new file mode 100644 index 0000000000..2370427eea --- /dev/null +++ b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_1.def.json @@ -0,0 +1,16 @@ +{ + "version": 2, + "name": "ACE Pro Color 2", + "inherits": "fdmextruder", + "metadata": + { + "machine": "anycubic_kobra3v2_ACE_PRO", + "position": "1" + }, + "overrides": + { + "extruder_nr": { "default_value": 1 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} \ No newline at end of file diff --git a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_2.def.json b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_2.def.json new file mode 100644 index 0000000000..ae860e5f7a --- /dev/null +++ b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_2.def.json @@ -0,0 +1,16 @@ +{ + "version": 2, + "name": "ACE Pro Color 3", + "inherits": "fdmextruder", + "metadata": + { + "machine": "anycubic_kobra3v2_ACE_PRO", + "position": "2" + }, + "overrides": + { + "extruder_nr": { "default_value": 2 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} \ No newline at end of file diff --git a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_3.def.json b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_3.def.json new file mode 100644 index 0000000000..fed2c1fc6b --- /dev/null +++ b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_3.def.json @@ -0,0 +1,16 @@ +{ + "version": 2, + "name": "ACE Pro Color 4", + "inherits": "fdmextruder", + "metadata": + { + "machine": "anycubic_kobra3v2_ACE_PRO", + "position": "3" + }, + "overrides": + { + "extruder_nr": { "default_value": 3 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} \ No newline at end of file diff --git a/resources/extruders/anycubic_kobra3v2_extruder_0.def.json b/resources/extruders/anycubic_kobra3v2_extruder_0.def.json new file mode 100644 index 0000000000..f5983cf3fb --- /dev/null +++ b/resources/extruders/anycubic_kobra3v2_extruder_0.def.json @@ -0,0 +1,16 @@ +{ + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": + { + "machine": "anycubic_kobra3v2", + "position": "0" + }, + "overrides": + { + "extruder_nr": { "default_value": 0 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} \ No newline at end of file diff --git a/resources/extruders/sovol_sv08_extruder.def.json b/resources/extruders/sovol_sv08_extruder.def.json new file mode 100644 index 0000000000..d0ccdee1de --- /dev/null +++ b/resources/extruders/sovol_sv08_extruder.def.json @@ -0,0 +1,19 @@ +{ + "version": 2, + "name": "Nozzle Size", + "inherits": "fdmextruder", + "metadata": + { + "machine": "sovol_sv08", + "position": "0" + }, + "overrides": + { + "extruder_nr": + { + "default_value": 0, + "maximum_value": 1 + }, + "material_diameter": { "default_value": 1.75 } + } +} \ No newline at end of file diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm_quick.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm_quick.inst.cfg new file mode 100644 index 0000000000..9889424797 --- /dev/null +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm_quick.inst.cfg @@ -0,0 +1,22 @@ +[general] +definition = ultimaker_s8 +name = Quick +version = 4 + +[metadata] +intent_category = quick +material = generic_abs +quality_type = draft +setting_version = 25 +type = intent +variant = AA+ 0.4 + +[values] +cool_min_layer_time = 5 +cool_min_layer_time_overhang = 9 +cool_min_speed = 6 +cool_min_temperature = =material_print_temperature - 15 +speed_wall_x = =speed_print +speed_wall_x_roofing = =speed_wall +wall_line_width_x = =wall_line_width + diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg new file mode 100644 index 0000000000..21e814e112 --- /dev/null +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg @@ -0,0 +1,18 @@ +[general] +definition = ultimaker_s8 +name = Accurate +version = 4 + +[metadata] +intent_category = engineering +material = generic_cpe_plus +quality_type = draft +setting_version = 25 +type = intent +variant = AA+ 0.4 + +[values] +infill_sparse_density = 20 +top_bottom_thickness = =wall_thickness +wall_thickness = =line_width * 4 + diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm_engineering.inst.cfg new file mode 100644 index 0000000000..8332ecacbc --- /dev/null +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm_engineering.inst.cfg @@ -0,0 +1,18 @@ +[general] +definition = ultimaker_s8 +name = Accurate +version = 4 + +[metadata] +intent_category = engineering +material = generic_pc +quality_type = draft +setting_version = 25 +type = intent +variant = AA+ 0.4 + +[values] +infill_sparse_density = 20 +top_bottom_thickness = =wall_thickness +wall_thickness = =line_width * 4 + diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm_quick.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm_quick.inst.cfg new file mode 100644 index 0000000000..ae38c02395 --- /dev/null +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm_quick.inst.cfg @@ -0,0 +1,22 @@ +[general] +definition = ultimaker_s8 +name = Quick +version = 4 + +[metadata] +intent_category = quick +material = generic_petg +quality_type = draft +setting_version = 25 +type = intent +variant = AA+ 0.4 + +[values] +cool_min_layer_time = 5 +cool_min_layer_time_overhang = 9 +cool_min_speed = 6 +cool_min_temperature = =material_print_temperature - 15 +speed_wall_x = =speed_print +speed_wall_x_roofing = =speed_wall +wall_line_width_x = =wall_line_width + diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm_quick.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm_quick.inst.cfg new file mode 100644 index 0000000000..4509317076 --- /dev/null +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm_quick.inst.cfg @@ -0,0 +1,22 @@ +[general] +definition = ultimaker_s8 +name = Quick +version = 4 + +[metadata] +intent_category = quick +material = generic_pla +quality_type = draft +setting_version = 25 +type = intent +variant = AA+ 0.4 + +[values] +cool_min_layer_time = 5 +cool_min_layer_time_overhang = 9 +cool_min_speed = 6 +cool_min_temperature = =material_print_temperature - 15 +speed_wall_x = =speed_print +speed_wall_x_roofing = =speed_wall +wall_line_width_x = =wall_line_width + diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm_quick.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm_quick.inst.cfg new file mode 100644 index 0000000000..4f75b631df --- /dev/null +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm_quick.inst.cfg @@ -0,0 +1,22 @@ +[general] +definition = ultimaker_s8 +name = Quick +version = 4 + +[metadata] +intent_category = quick +material = generic_tough_pla +quality_type = draft +setting_version = 25 +type = intent +variant = AA+ 0.4 + +[values] +cool_min_layer_time = 5 +cool_min_layer_time_overhang = 9 +cool_min_speed = 6 +cool_min_temperature = =material_print_temperature - 15 +speed_wall_x = =speed_print +speed_wall_x_roofing = =speed_wall +wall_line_width_x = =wall_line_width + diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg index a474c19cf7..832912a022 100644 --- a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg @@ -5,6 +5,7 @@ version = 4 [metadata] intent_category = engineering +is_experimental = True material = generic_cpe_plus quality_type = draft setting_version = 25 diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm_engineering.inst.cfg index e5ccc32e49..a0e65969e9 100644 --- a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm_engineering.inst.cfg @@ -5,6 +5,7 @@ version = 4 [metadata] intent_category = engineering +is_experimental = True material = generic_nylon-cf-slide quality_type = draft setting_version = 25 diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm_engineering.inst.cfg index cecb670819..f40d2509b6 100644 --- a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm_engineering.inst.cfg @@ -5,6 +5,7 @@ version = 4 [metadata] intent_category = engineering +is_experimental = True material = generic_pc quality_type = draft setting_version = 25 diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm_engineering.inst.cfg index 0819ec9c81..824018a1d5 100644 --- a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm_engineering.inst.cfg @@ -5,6 +5,7 @@ version = 4 [metadata] intent_category = engineering +is_experimental = True material = generic_petcf quality_type = draft setting_version = 25 diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg new file mode 100644 index 0000000000..8cbb513108 --- /dev/null +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg @@ -0,0 +1,18 @@ +[general] +definition = ultimaker_s8 +name = Accurate +version = 4 + +[metadata] +intent_category = engineering +material = generic_nylon-cf-slide +quality_type = draft +setting_version = 25 +type = intent +variant = CC+ 0.6 + +[values] +infill_sparse_density = 20 +top_bottom_thickness = =wall_thickness +wall_thickness = =line_width * 4 + diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg new file mode 100644 index 0000000000..5384f380ac --- /dev/null +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg @@ -0,0 +1,18 @@ +[general] +definition = ultimaker_s8 +name = Accurate +version = 4 + +[metadata] +intent_category = engineering +material = generic_petcf +quality_type = draft +setting_version = 25 +type = intent +variant = CC+ 0.6 + +[values] +infill_sparse_density = 20 +top_bottom_thickness = =wall_thickness +wall_thickness = =line_width * 4 + diff --git a/resources/meshes/anycubic_kobra3v2_buildplate.stl b/resources/meshes/anycubic_kobra3v2_buildplate.stl new file mode 100644 index 0000000000..9d526d0eda Binary files /dev/null and b/resources/meshes/anycubic_kobra3v2_buildplate.stl differ diff --git a/resources/meshes/sovol_sv08_buildplate_model.stl b/resources/meshes/sovol_sv08_buildplate_model.stl new file mode 100644 index 0000000000..91acb50bd4 Binary files /dev/null and b/resources/meshes/sovol_sv08_buildplate_model.stl differ diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 77efc45fe8..bc5cc6a044 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -456,45 +456,32 @@ UM.MainWindow } } - UM.PreferencesDialog + Component { - id: preferences - - Component.onCompleted: + id: preferencesDialogComponent + Cura.PreferencesDialog { - //; Remove & re-add the general page as we want to use our own instead of uranium standard. - removePage(0); - insertPage(0, catalog.i18nc("@title:tab","General"), Qt.resolvedUrl("Preferences/GeneralPage.qml")); - - removePage(1); - insertPage(1, catalog.i18nc("@title:tab","Settings"), Qt.resolvedUrl("Preferences/SettingVisibilityPage.qml")); - - insertPage(2, catalog.i18nc("@title:tab", "Printers"), Qt.resolvedUrl("Preferences/MachinesPage.qml")); - - insertPage(3, catalog.i18nc("@title:tab", "Materials"), Qt.resolvedUrl("Preferences/Materials/MaterialsPage.qml")); - - insertPage(4, catalog.i18nc("@title:tab", "Profiles"), Qt.resolvedUrl("Preferences/ProfilesPage.qml")); - currentPage = 0; + selfDestroy: true } + } - onVisibleChanged: - { - // When the dialog closes, switch to the General page. - // This prevents us from having a heavy page like Setting Visibility active in the background. - setPage(0); - } + function showPreferencesDialog() + { + var dialog = preferencesDialogComponent.createObject(base) + dialog.show() + return dialog } Connections { target: Cura.Actions.preferences - function onTriggered() { preferences.visible = true } + function onTriggered() { showPreferencesDialog() } } Connections { target: CuraApplication - function onShowPreferencesWindow() { preferences.visible = true } + function onShowPreferencesWindow() { showPreferencesDialog() } } Connections @@ -511,8 +498,8 @@ UM.MainWindow target: Cura.Actions.configureMachines function onTriggered() { - preferences.visible = true; - preferences.setPage(2); + var dialog = showPreferencesDialog() + dialog.currentPage = 2; } } @@ -521,8 +508,8 @@ UM.MainWindow target: Cura.Actions.manageProfiles function onTriggered() { - preferences.visible = true; - preferences.setPage(4); + var dialog = showPreferencesDialog() + dialog.currentPage = 4; } } @@ -531,8 +518,8 @@ UM.MainWindow target: Cura.Actions.manageMaterials function onTriggered() { - preferences.visible = true; - preferences.setPage(3) + var dialog = showPreferencesDialog() + dialog.currentPage = 3; } } @@ -541,11 +528,11 @@ UM.MainWindow target: Cura.Actions.configureSettingVisibility function onTriggered(source) { - preferences.visible = true; - preferences.setPage(1); + var dialog = showPreferencesDialog() + dialog.currentPage = 1; if(source && source.key) { - preferences.getCurrentItem().scrollToSection(source.key); + dialog.currentItem.scrollToSection(source.key); } } } diff --git a/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelectorButton.qml b/resources/qml/ModeSelectorButton.qml similarity index 91% rename from resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelectorButton.qml rename to resources/qml/ModeSelectorButton.qml index 1bbc726b9d..65a6ee4a75 100644 --- a/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelectorButton.qml +++ b/resources/qml/ModeSelectorButton.qml @@ -17,7 +17,7 @@ Rectangle color: mouseArea.containsMouse || selected ? UM.Theme.getColor("background_3") : UM.Theme.getColor("background_1") property bool selected: false - property string profileName: "" + property alias text: mainLabel.text property string icon: "" property string custom_icon: "" property alias tooltipText: tooltip.text @@ -42,18 +42,18 @@ Rectangle Item { - width: intentIcon.width + width: mainIcon.width anchors { top: parent.top - bottom: qualityLabel.top + bottom: mainLabel.top horizontalCenter: parent.horizontalCenter topMargin: UM.Theme.getSize("narrow_margin").height } Item { - id: intentIcon + id: mainIcon width: UM.Theme.getSize("recommended_button_icon").width height: UM.Theme.getSize("recommended_button_icon").height @@ -90,7 +90,7 @@ Rectangle { id: initialLabel anchors.centerIn: parent - text: profileName.charAt(0).toUpperCase() + text: base.text.charAt(0).toUpperCase() font: UM.Theme.getFont("small_bold") horizontalAlignment: Text.AlignHCenter } @@ -102,8 +102,7 @@ Rectangle UM.Label { - id: qualityLabel - text: profileName + id: mainLabel anchors { bottom: parent.bottom diff --git a/resources/qml/Preferences/PreferencesDialog.qml b/resources/qml/Preferences/PreferencesDialog.qml new file mode 100644 index 0000000000..f175370205 --- /dev/null +++ b/resources/qml/Preferences/PreferencesDialog.qml @@ -0,0 +1,130 @@ +// Copyright (c) 2022 Ultimaker B.V. +// Uranium is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.1 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.1 + +import ".." + +import UM 1.6 as UM + +UM.Dialog +{ + id: base + + title: catalog.i18nc("@title:window", "Preferences") + minimumWidth: UM.Theme.getSize("modal_window_minimum").width + minimumHeight: UM.Theme.getSize("modal_window_minimum").height + width: minimumWidth + height: minimumHeight + backgroundColor: UM.Theme.getColor("background_2") + + property alias currentPage: pagesList.currentIndex + property alias currentItem: pagesList.currentItem + + Item + { + id: test + anchors.fill: parent + + ListView + { + id: pagesList + width: UM.Theme.getSize("preferences_page_list_item").width + anchors.top: parent.top + anchors.bottom: parent.bottom + + ScrollBar.vertical: UM.ScrollBar {} + clip: true + model: [ + { + name: catalog.i18nc("@title:tab", "General"), + item: Qt.resolvedUrl("GeneralPage.qml") + }, + { + name: catalog.i18nc("@title:tab", "Settings"), + item: Qt.resolvedUrl("SettingVisibilityPage.qml") + }, + { + name: catalog.i18nc("@title:tab", "Printers"), + item: Qt.resolvedUrl("MachinesPage.qml") + }, + { + name: catalog.i18nc("@title:tab", "Materials"), + item: Qt.resolvedUrl("Materials/MaterialsPage.qml") + }, + { + name: catalog.i18nc("@title:tab", "Profiles"), + item: Qt.resolvedUrl("ProfilesPage.qml") + } + ] + + delegate: Rectangle + { + width: parent ? parent.width : 0 + height: pageLabel.height + + color: ListView.isCurrentItem ? UM.Theme.getColor("background_3") : UM.Theme.getColor("main_background") + + UM.Label + { + id: pageLabel + anchors.centerIn: parent + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + width: parent.width + height: UM.Theme.getSize("preferences_page_list_item").height + color: UM.Theme.getColor("text_default") + text: modelData.name + } + MouseArea + { + anchors.fill: parent + onClicked: pagesList.currentIndex = index + } + } + + onCurrentIndexChanged: stackView.replace(model[currentIndex].item) + } + + StackView + { + id: stackView + anchors + { + left: pagesList.right + leftMargin: UM.Theme.getSize("narrow_margin").width + top: parent.top + bottom: parent.bottom + right: parent.right + } + + initialItem: Item { property bool resetEnabled: false } + + replaceEnter: Transition + { + NumberAnimation + { + properties: "opacity" + from: 0 + to: 1 + duration: 100 + } + } + replaceExit: Transition + { + NumberAnimation + { + properties: "opacity" + from: 1 + to: 0 + duration: 100 + } + } + } + + UM.I18nCatalog { id: catalog; name: "uranium"; } + } +} diff --git a/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml b/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml index 19c57e5130..1559f6cec3 100644 --- a/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml +++ b/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml @@ -7,7 +7,6 @@ import QtQuick.Layouts 2.10 import UM 1.5 as UM import Cura 1.7 as Cura -import ".." Item { @@ -28,9 +27,9 @@ Item id: intentSelectionRepeater model: Cura.IntentSelectionModel {} - RecommendedQualityProfileSelectorButton + Cura.ModeSelectorButton { - profileName: model.name + text: model.name icon: model.icon ? model.icon : "" custom_icon: model.custom_icon ? model.custom_icon : "" tooltipText: model.description ? model.description : "" diff --git a/resources/qml/PrinterSelector/MachineSelector.qml b/resources/qml/PrinterSelector/MachineSelector.qml index 1bad1d70bc..e8ee98fe8f 100644 --- a/resources/qml/PrinterSelector/MachineSelector.qml +++ b/resources/qml/PrinterSelector/MachineSelector.qml @@ -16,6 +16,7 @@ Cura.ExpandablePopup property bool isConnectedCloudPrinter: machineManager.activeMachineHasCloudConnection property bool isCloudRegistered: machineManager.activeMachineHasCloudRegistration property bool isGroup: machineManager.activeMachineIsGroup + property bool isActive: machineManager.activeMachineIsActive property string machineName: { if (isNetworkPrinter && machineManager.activeMachineNetworkGroupName != "") { @@ -40,7 +41,14 @@ Cura.ExpandablePopup } else if (isConnectedCloudPrinter && Cura.API.connectionStatus.isInternetReachable) { - return "printer_cloud_connected" + if (isActive) + { + return "printer_cloud_connected" + } + else + { + return "printer_cloud_inactive" + } } else if (isCloudRegistered) { @@ -53,7 +61,7 @@ Cura.ExpandablePopup } function getConnectionStatusMessage() { - if (connectionStatus == "printer_cloud_not_available") + if (connectionStatus === "printer_cloud_not_available") { if(Cura.API.connectionStatus.isInternetReachable) { @@ -78,6 +86,10 @@ Cura.ExpandablePopup return catalog.i18nc("@status", "The cloud connection is currently unavailable. Please check your internet connection.") } } + else if(connectionStatus === "printer_cloud_inactive") + { + return catalog.i18nc("@status", "This printer is deactivated and can not accept commands or jobs.") + } else { return "" @@ -130,14 +142,18 @@ Cura.ExpandablePopup source: { - if (connectionStatus == "printer_connected") + if (connectionStatus === "printer_connected") { return UM.Theme.getIcon("CheckBlueBG", "low") } - else if (connectionStatus == "printer_cloud_connected" || connectionStatus == "printer_cloud_not_available") + else if (connectionStatus === "printer_cloud_connected" || connectionStatus === "printer_cloud_not_available") { return UM.Theme.getIcon("CloudBadge", "low") } + else if (connectionStatus === "printer_cloud_inactive") + { + return UM.Theme.getIcon("WarningBadge", "low") + } else { return "" @@ -147,7 +163,21 @@ Cura.ExpandablePopup width: UM.Theme.getSize("printer_status_icon").width height: UM.Theme.getSize("printer_status_icon").height - color: connectionStatus == "printer_cloud_not_available" ? UM.Theme.getColor("cloud_unavailable") : UM.Theme.getColor("primary") + color: + { + if (connectionStatus === "printer_cloud_not_available") + { + return UM.Theme.getColor("cloud_unavailable") + } + else if(connectionStatus === "printer_cloud_inactive") + { + return UM.Theme.getColor("cloud_inactive") + } + else + { + return UM.Theme.getColor("primary") + } + } visible: (isNetworkPrinter || isCloudRegistered) && source != "" diff --git a/resources/qml/ViewsSelector.qml b/resources/qml/ViewsSelector.qml index e76e5dbb67..b0e31ac532 100644 --- a/resources/qml/ViewsSelector.qml +++ b/resources/qml/ViewsSelector.qml @@ -38,7 +38,7 @@ Cura.ExpandablePopup { if (activeView == null) { - UM.Controller.setActiveView(viewModel.getItem(0).id) + UM.Controller.activeStage.setActiveView(viewModel.getItem(0).id) } } @@ -110,7 +110,7 @@ Cura.ExpandablePopup onClicked: { toggleContent() - UM.Controller.setActiveView(id) + UM.Controller.activeStage.setActiveView(id) } } } diff --git a/resources/qml/qmldir b/resources/qml/qmldir index 8fce82c858..98140608c6 100644 --- a/resources/qml/qmldir +++ b/resources/qml/qmldir @@ -52,3 +52,7 @@ NumericTextFieldWithUnit 1.0 NumericTextFieldWithUnit.qml PrintHeadMinMaxTextField 1.0 PrintHeadMinMaxTextField.qml SimpleCheckBox 1.0 SimpleCheckBox.qml RenameDialog 1.0 RenameDialog.qml + +# Cura/Preferences + +PreferencesDialog 1.0 PreferencesDialog.qml diff --git a/resources/quality/normal.inst.cfg b/resources/quality/normal.inst.cfg index 4ca290b0b5..5d82bf612c 100644 --- a/resources/quality/normal.inst.cfg +++ b/resources/quality/normal.inst.cfg @@ -11,4 +11,5 @@ type = quality weight = 0 [values] +layer_height = 0.1 diff --git a/resources/quality/sovol/ABS/sovol_sv08_0.4_ABS_standard.inst.cfg b/resources/quality/sovol/ABS/sovol_sv08_0.4_ABS_standard.inst.cfg new file mode 100644 index 0000000000..4fdd35f5ba --- /dev/null +++ b/resources/quality/sovol/ABS/sovol_sv08_0.4_ABS_standard.inst.cfg @@ -0,0 +1,27 @@ +[general] +definition = sovol_sv08 +name = Standard Quality +version = 4 + +[metadata] +material = generic_abs +quality_type = standard +setting_version = 25 +type = quality +variant = 0.4mm Nozzle + +[values] +bridge_fan_speed = 30 +bridge_settings_enabled = True +cool_fan_enabled = True +cool_fan_speed = 10 +cool_fan_speed_max = 30 +cool_min_layer_time = 4 +cool_min_layer_time_fan_speed_max = 30 +cool_min_speed = 10 +material_bed_temperature = 95 +material_flow = 98 +material_max_flowrate = 21 +material_print_temperature = 270 +material_print_temperature_layer_0 = 280 + diff --git a/resources/quality/sovol/PETG/sovol_sv08_0.4_PETG_standard.inst.cfg b/resources/quality/sovol/PETG/sovol_sv08_0.4_PETG_standard.inst.cfg new file mode 100644 index 0000000000..3cf9d68d04 --- /dev/null +++ b/resources/quality/sovol/PETG/sovol_sv08_0.4_PETG_standard.inst.cfg @@ -0,0 +1,27 @@ +[general] +definition = sovol_sv08 +name = Standard Quality +version = 4 + +[metadata] +material = generic_petg +quality_type = standard +setting_version = 25 +type = quality +variant = 0.4mm Nozzle + +[values] +bridge_fan_speed = 70 +bridge_settings_enabled = True +cool_fan_enabled = True +cool_fan_speed = 10 +cool_fan_speed_max = 30 +cool_min_layer_time = 5 +cool_min_layer_time_fan_speed_max = 30 +cool_min_speed = 10 +material_bed_temperature = 75 +material_flow = 98 +material_max_flowrate = 17 +material_print_temperature = 235 +material_print_temperature_layer_0 = 250 + diff --git a/resources/quality/sovol/PLA/sovol_sv08_0.4_PLA_standard.inst.cfg b/resources/quality/sovol/PLA/sovol_sv08_0.4_PLA_standard.inst.cfg new file mode 100644 index 0000000000..daca9ebd94 --- /dev/null +++ b/resources/quality/sovol/PLA/sovol_sv08_0.4_PLA_standard.inst.cfg @@ -0,0 +1,27 @@ +[general] +definition = sovol_sv08 +name = Standard Quality +version = 4 + +[metadata] +material = generic_pla +quality_type = standard +setting_version = 25 +type = quality +variant = 0.4mm Nozzle + +[values] +bridge_fan_speed = 100 +bridge_settings_enabled = True +cool_fan_enabled = True +cool_fan_speed = 50 +cool_fan_speed_max = 70 +cool_min_layer_time = 5 +cool_min_layer_time_fan_speed_max = 50 +cool_min_speed = 10 +material_bed_temperature = 65 +material_flow = 98 +material_max_flowrate = 21 +material_print_temperature = 220 +material_print_temperature_layer_0 = 235 + diff --git a/resources/quality/sovol/TPU/sovol_sv08_0.4_TPU_standard.inst.cfg b/resources/quality/sovol/TPU/sovol_sv08_0.4_TPU_standard.inst.cfg new file mode 100644 index 0000000000..c1b44b46b0 --- /dev/null +++ b/resources/quality/sovol/TPU/sovol_sv08_0.4_TPU_standard.inst.cfg @@ -0,0 +1,27 @@ +[general] +definition = sovol_sv08 +name = Standard Quality +version = 4 + +[metadata] +material = generic_tpu +quality_type = standard +setting_version = 25 +type = quality +variant = 0.4mm Nozzle + +[values] +bridge_fan_speed = 100 +bridge_settings_enabled = True +cool_fan_enabled = True +cool_fan_speed = 80 +cool_fan_speed_max = 100 +cool_min_layer_time = 5 +cool_min_layer_time_fan_speed_max = 50 +cool_min_speed = 10 +material_bed_temperature = 65 +material_flow = 98 +material_max_flowrate = 3.6 +material_print_temperature = 240 +material_print_temperature_layer_0 = 235 + diff --git a/resources/quality/sovol/sovol_sv08_global.inst.cfg b/resources/quality/sovol/sovol_sv08_global.inst.cfg new file mode 100644 index 0000000000..4030ec9441 --- /dev/null +++ b/resources/quality/sovol/sovol_sv08_global.inst.cfg @@ -0,0 +1,31 @@ +[general] +definition = sovol_sv08 +name = 0.20mm Standard +version = 4 + +[metadata] +global_quality = True +quality_type = standard +setting_version = 25 +type = quality + +[values] +acceleration_enabled = True +acceleration_layer_0 = 3000 +acceleration_print = 20000 +acceleration_roofing = =acceleration_wall_0 +acceleration_topbottom = =acceleration_wall +acceleration_travel = 40000 +acceleration_wall_0 = 8000 +acceleration_wall_x = 12000 +layer_height = 0.2 +skirt_brim_speed = 80 +speed_infill = 200 +speed_ironing = 15 +speed_layer_0 = 30 +speed_print = 600 +speed_slowdown_layers = 3 +speed_travel = =speed_print +speed_wall_0 = 200 +speed_wall_x = 300 + diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm.inst.cfg index 37767673aa..cc5e850220 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm.inst.cfg @@ -14,7 +14,10 @@ weight = -2 [values] cool_min_layer_time = 4 cool_min_layer_time_fan_speed_max = 9 -cool_min_temperature = =material_print_temperature - 10 +cool_min_temperature = =material_print_temperature - 20 retraction_prime_speed = 15 +speed_wall_x = =speed_wall +speed_wall_x_roofing = =speed_wall * 0.8 support_structure = tree +wall_line_width_x = =wall_line_width * 1.25 diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm.inst.cfg new file mode 100644 index 0000000000..9ca2677c01 --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm.inst.cfg @@ -0,0 +1,19 @@ +[general] +definition = ultimaker_s8 +name = Fast +version = 4 + +[metadata] +material = generic_cpe_plus +quality_type = draft +setting_version = 25 +type = quality +variant = AA+ 0.4 +weight = -2 + +[values] +adhesion_type = brim +material_alternate_walls = True +material_final_print_temperature = =material_print_temperature - 15 +material_initial_print_temperature = =material_print_temperature - 15 + diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm.inst.cfg new file mode 100644 index 0000000000..0709fe6c6c --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm.inst.cfg @@ -0,0 +1,25 @@ +[general] +definition = ultimaker_s8 +name = Fast +version = 4 + +[metadata] +material = generic_pc +quality_type = draft +setting_version = 25 +type = quality +variant = AA+ 0.4 +weight = -2 + +[values] +adhesion_type = brim +cool_min_layer_time = 6 +cool_min_layer_time_fan_speed_max = 12 +inset_direction = inside_out +material_alternate_walls = True +material_final_print_temperature = =material_print_temperature - 15 +material_flow = 95 +material_initial_print_temperature = =material_print_temperature - 15 +retraction_prime_speed = 15 +speed_wall_x = =speed_wall_0 + diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm.inst.cfg index baabd79e94..9abcd5ddd2 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm.inst.cfg @@ -15,4 +15,7 @@ weight = -2 cool_min_layer_time = 4 material_print_temperature = =default_material_print_temperature + 5 retraction_prime_speed = 15 +speed_wall_x = =speed_wall +speed_wall_x_roofing = =speed_wall * 0.8 +wall_line_width_x = =wall_line_width * 1.25 diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.15mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.15mm.inst.cfg index 6c445180f8..9feab61e0e 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.15mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.15mm.inst.cfg @@ -12,8 +12,12 @@ variant = AA+ 0.4 weight = -1 [values] +cool_min_temperature = =material_print_temperature - 20 material_final_print_temperature = =material_print_temperature - 15 material_initial_print_temperature = =material_print_temperature - 15 retraction_prime_speed = =retraction_speed +speed_wall_x = =speed_wall +speed_wall_x_roofing = =speed_wall * 0.8 support_structure = tree +wall_line_width_x = =wall_line_width * 1.25 diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.1mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.1mm.inst.cfg index d3d99eec9e..8431bb9c43 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.1mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.1mm.inst.cfg @@ -12,9 +12,13 @@ variant = AA+ 0.4 weight = 0 [values] +cool_min_temperature = =material_print_temperature - 20 material_final_print_temperature = =material_print_temperature - 15 material_initial_print_temperature = =material_print_temperature - 15 retraction_prime_speed = =retraction_speed +speed_wall_x = =speed_wall +speed_wall_x_roofing = =speed_wall * 0.8 support_structure = tree top_bottom_thickness = =round(6*layer_height,3) +wall_line_width_x = =wall_line_width * 1.25 diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm.inst.cfg index 2e015f8a88..7a5d19dc2c 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm.inst.cfg @@ -12,8 +12,12 @@ variant = AA+ 0.4 weight = -2 [values] +cool_min_temperature = =material_print_temperature - 20 material_final_print_temperature = =material_print_temperature - 15 material_initial_print_temperature = =material_print_temperature - 15 retraction_prime_speed = =retraction_speed +speed_wall_x = =speed_wall +speed_wall_x_roofing = =speed_wall * 0.8 support_structure = tree +wall_line_width_x = =wall_line_width * 1.25 diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.15mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.15mm.inst.cfg index e3477c1e7d..f17d3fde40 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.15mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.15mm.inst.cfg @@ -12,7 +12,11 @@ variant = AA+ 0.4 weight = -1 [values] +cool_min_temperature = =material_print_temperature - 20 retraction_prime_speed = =retraction_speed retraction_speed = 25 +speed_wall_x = =speed_wall +speed_wall_x_roofing = =speed_wall * 0.8 support_structure = tree +wall_line_width_x = =wall_line_width * 1.25 diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.1mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.1mm.inst.cfg index 6ccc0da6dd..672eae3e4a 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.1mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.1mm.inst.cfg @@ -12,7 +12,11 @@ variant = AA+ 0.4 weight = 0 [values] +cool_min_temperature = =material_print_temperature - 20 retraction_prime_speed = =retraction_speed +speed_wall_x = =speed_wall +speed_wall_x_roofing = =speed_wall * 0.8 support_structure = tree top_bottom_thickness = =round(6*layer_height,3) +wall_line_width_x = =wall_line_width * 1.25 diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm.inst.cfg index bb629e0758..716765aac5 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm.inst.cfg @@ -12,6 +12,10 @@ variant = AA+ 0.4 weight = -2 [values] +cool_min_temperature = =material_print_temperature - 20 retraction_prime_speed = =retraction_speed +speed_wall_x = =speed_wall +speed_wall_x_roofing = =speed_wall * 0.8 support_structure = tree +wall_line_width_x = =wall_line_width * 1.25 diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm.inst.cfg index 3475b01999..80d65e7955 100644 --- a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm.inst.cfg @@ -1,9 +1,10 @@ [general] definition = ultimaker_s8 -name = Fast +name = Fast - Experimental version = 4 [metadata] +is_experimental = True material = generic_cpe_plus quality_type = draft setting_version = 25 diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm.inst.cfg index 1085302fc7..5f3180ef4e 100644 --- a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm.inst.cfg @@ -1,9 +1,10 @@ [general] definition = ultimaker_s8 -name = Fast +name = Fast - Experimental version = 4 [metadata] +is_experimental = True material = generic_nylon-cf-slide quality_type = draft setting_version = 25 @@ -12,6 +13,12 @@ variant = CC+ 0.4 weight = -2 [values] +bridge_skin_material_flow = 100 +bridge_skin_speed = 30 +bridge_wall_material_flow = 100 +bridge_wall_speed = 30 cool_min_layer_time_fan_speed_max = 11 retraction_prime_speed = 15 +support_structure = tree +wall_overhang_speed_factors = [100,90,80,70,60,50] diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm.inst.cfg index bb73d83750..540d62e154 100644 --- a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm.inst.cfg @@ -1,9 +1,10 @@ [general] definition = ultimaker_s8 -name = Fast +name = Fast - Experimental version = 4 [metadata] +is_experimental = True material = generic_pc quality_type = draft setting_version = 25 diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm.inst.cfg index 0038bb0a4d..3467ed5ded 100644 --- a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm.inst.cfg @@ -1,9 +1,10 @@ [general] definition = ultimaker_s8 -name = Fast +name = Fast - Experimental version = 4 [metadata] +is_experimental = True material = generic_petcf quality_type = draft setting_version = 25 @@ -12,5 +13,12 @@ variant = CC+ 0.4 weight = -2 [values] +adhesion_type = skirt +bridge_skin_material_flow = 100 +bridge_skin_speed = 30 +bridge_wall_material_flow = 100 +bridge_wall_speed = 30 +support_structure = tree switch_extruder_retraction_amount = 16 +wall_overhang_speed_factors = [100,90,80,70,60,50] diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm.inst.cfg new file mode 100644 index 0000000000..eb29199252 --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm.inst.cfg @@ -0,0 +1,60 @@ +[general] +definition = ultimaker_s8 +name = Fast +version = 4 + +[metadata] +material = generic_nylon-cf-slide +quality_type = draft +setting_version = 25 +type = quality +variant = CC+ 0.6 +weight = -2 + +[values] +acceleration_roofing = =acceleration_topbottom/2 +bridge_enable_more_layers = True +bridge_skin_density = 70 +bridge_skin_material_flow = 100 +bridge_skin_material_flow_2 = 70 +bridge_skin_speed = 30 +bridge_skin_speed_2 = =speed_print*2/3 +bridge_wall_material_flow = 100 +bridge_wall_min_length = 2 +bridge_wall_speed = 30 +cool_min_layer_time = 6 +cool_min_layer_time_fan_speed_max = 11 +cool_min_layer_time_overhang = 11 +cool_min_temperature = =material_print_temperature-10 +flooring_monotonic = False +infill_material_flow = =material_flow if infill_sparse_density < 95 else 95 +infill_pattern = ='zigzag' if infill_sparse_density > 50 else 'grid' +jerk_roofing = =jerk_print +material_flow = 95 +retraction_hop_enabled = False +retraction_prime_speed = 25 +roofing_line_width = 0.5 +roofing_material_flow = =skin_material_flow +roofing_monotonic = False +skin_material_flow = =0.95*material_flow +skin_outline_count = 0 +speed_print = 80 +speed_roofing = 50 +speed_wall = =speed_print +speed_wall_0_roofing = =speed_roofing +speed_wall_x_roofing = =speed_roofing +support_bottom_distance = =support_z_distance +support_line_width = 0.6 +support_structure = tree +support_tree_tip_diameter = 2.0 +support_tree_top_rate = 10 +support_xy_distance = 1.2 +support_xy_distance_overhang = =1.5*machine_nozzle_size +support_z_distance = =min(2*layer_height, 0.4) +top_bottom_thickness = =wall_thickness +wall_0_inset = =0.05 +wall_line_width_0 = 0.5 +wall_overhang_speed_factors = [100,90,80,70,60,50] +wall_x_material_flow = =material_flow +xy_offset = 0.075 + diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm.inst.cfg new file mode 100644 index 0000000000..b568f01cde --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm.inst.cfg @@ -0,0 +1,63 @@ +[general] +definition = ultimaker_s8 +name = Fast +version = 4 + +[metadata] +material = generic_petcf +quality_type = draft +setting_version = 25 +type = quality +variant = CC+ 0.6 +weight = -2 + +[values] +acceleration_roofing = =acceleration_topbottom/2 +adhesion_type = skirt +bridge_enable_more_layers = True +bridge_skin_density = 70 +bridge_skin_material_flow = 100 +bridge_skin_material_flow_2 = 70 +bridge_skin_speed = 30 +bridge_skin_speed_2 = =speed_print*2/3 +bridge_wall_material_flow = 100 +bridge_wall_min_length = 2 +bridge_wall_speed = 30 +cool_min_layer_time = 6 +cool_min_layer_time_overhang = 11 +cool_min_temperature = =material_print_temperature-10 +flooring_monotonic = False +infill_material_flow = =material_flow if infill_sparse_density < 95 else 95 +infill_pattern = ='zigzag' if infill_sparse_density > 50 else 'grid' +jerk_roofing = =jerk_print +material_pressure_advance_factor = 0.25 +retraction_hop_enabled = False +retraction_prime_speed = 15 +roofing_line_width = 0.5 +roofing_material_flow = =skin_material_flow +roofing_monotonic = False +skin_material_flow = =0.95*material_flow +skin_outline_count = 0 +skirt_height = 5 +speed_print = 80 +speed_roofing = 50 +speed_wall = =speed_print +speed_wall_0_roofing = =speed_roofing +speed_wall_x_roofing = =speed_roofing +support_bottom_distance = =support_z_distance +support_interface_enable = False +support_line_width = 0.6 +support_structure = tree +support_tree_tip_diameter = 2.0 +support_tree_top_rate = 10 +support_xy_distance = 1.2 +support_xy_distance_overhang = =1.5*machine_nozzle_size +support_z_distance = =min(2*layer_height, 0.4) +switch_extruder_retraction_amount = 16 +top_bottom_thickness = =wall_thickness +wall_0_inset = =0.05 +wall_line_width_0 = 0.5 +wall_overhang_speed_factors = [100,90,80,70,60,50] +wall_x_material_flow = =material_flow +xy_offset = 0.075 + diff --git a/resources/texts/change_log.txt b/resources/texts/change_log.txt index c432a9e309..af983e621c 100644 --- a/resources/texts/change_log.txt +++ b/resources/texts/change_log.txt @@ -1,3 +1,10 @@ +[5.10.2] + +* UltiMaker S6 and S8 improvements: +- Introduced the CC+ 0.6 core to the UltiMaker S6 and S8. This core delivers better results for demanding applications and will be replacing the CC+ 0.4 core. +- Added new profiles for PC and CPE+ on UltiMaker S6 and S8 +- Updated the default support type for the PETG material for UltiMaker S6 and S8 + [5.10.1] * New features and improvements: diff --git a/resources/themes/cura-light/icons/default/Circle.svg b/resources/themes/cura-light/icons/default/Circle.svg new file mode 100644 index 0000000000..c69b5a4e31 --- /dev/null +++ b/resources/themes/cura-light/icons/default/Circle.svg @@ -0,0 +1,5 @@ + + + + diff --git a/resources/themes/cura-light/icons/default/Eraser.svg b/resources/themes/cura-light/icons/default/Eraser.svg new file mode 100644 index 0000000000..fbe5103993 --- /dev/null +++ b/resources/themes/cura-light/icons/default/Eraser.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/themes/cura-light/icons/default/Seam.svg b/resources/themes/cura-light/icons/default/Seam.svg new file mode 100644 index 0000000000..a9615832d6 --- /dev/null +++ b/resources/themes/cura-light/icons/default/Seam.svg @@ -0,0 +1,6 @@ + + + + diff --git a/resources/themes/cura-light/icons/low/CancelBadge.svg b/resources/themes/cura-light/icons/low/CancelBadge.svg new file mode 100644 index 0000000000..25c4198083 --- /dev/null +++ b/resources/themes/cura-light/icons/low/CancelBadge.svg @@ -0,0 +1,5 @@ + + + + diff --git a/resources/themes/cura-light/icons/low/CheckBadge.svg b/resources/themes/cura-light/icons/low/CheckBadge.svg new file mode 100644 index 0000000000..a10a92c6af --- /dev/null +++ b/resources/themes/cura-light/icons/low/CheckBadge.svg @@ -0,0 +1,5 @@ + + + + diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 1ae316f96c..436aaceb3c 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -496,12 +496,17 @@ "monitor_carousel_dot_current": [119, 119, 119, 255], "cloud_unavailable": [153, 153, 153, 255], + "cloud_inactive": [253, 209, 58, 255], "connection_badge_background": [255, 255, 255, 255], "warning_badge_background": [0, 0, 0, 255], "error_badge_background": [255, 255, 255, 255], "border_field_light": [180, 180, 180, 255], - "border_main_light": [212, 212, 212, 255] + "border_main_light": [212, 212, 212, 255], + + "paint_normal_area": "background_3", + "paint_preferred_area": "um_green_5", + "paint_avoid_area": "um_red_5" }, "sizes": { diff --git a/resources/variants/sovol/sovol_sv08_0.4.inst.cfg b/resources/variants/sovol/sovol_sv08_0.4.inst.cfg new file mode 100644 index 0000000000..b5c72e92d3 --- /dev/null +++ b/resources/variants/sovol/sovol_sv08_0.4.inst.cfg @@ -0,0 +1,13 @@ +[general] +definition = sovol_sv08 +name = 0.4mm Nozzle +version = 4 + +[metadata] +hardware_type = nozzle +setting_version = 25 +type = variant + +[values] +machine_nozzle_size = 0.4 + diff --git a/resources/variants/ultimaker_s6_bb04.inst.cfg b/resources/variants/ultimaker_s6_bb04.inst.cfg index 756d6fd1d4..e0c62d9596 100644 --- a/resources/variants/ultimaker_s6_bb04.inst.cfg +++ b/resources/variants/ultimaker_s6_bb04.inst.cfg @@ -11,6 +11,7 @@ type = variant [values] machine_nozzle_heat_up_speed = 1.5 machine_nozzle_id = BB 0.4 +machine_nozzle_size = 0.4 machine_nozzle_tip_outer_diameter = 1.0 retraction_amount = 4.5 support_bottom_height = =layer_height * 2 diff --git a/resources/variants/ultimaker_s6_cc_plus06.inst.cfg b/resources/variants/ultimaker_s6_cc_plus06.inst.cfg new file mode 100644 index 0000000000..93564bada0 --- /dev/null +++ b/resources/variants/ultimaker_s6_cc_plus06.inst.cfg @@ -0,0 +1,17 @@ +[general] +definition = ultimaker_s6 +name = CC+ 0.6 +version = 4 + +[metadata] +hardware_type = nozzle +setting_version = 25 +type = variant + +[values] +machine_nozzle_cool_down_speed = 0.9 +machine_nozzle_id = CC+ 0.6 +machine_nozzle_size = 0.6 +machine_nozzle_tip_outer_diameter = 1.2 +retraction_prime_speed = =retraction_speed + diff --git a/resources/variants/ultimaker_s8_cc_plus06.inst.cfg b/resources/variants/ultimaker_s8_cc_plus06.inst.cfg new file mode 100644 index 0000000000..2a1c43873f --- /dev/null +++ b/resources/variants/ultimaker_s8_cc_plus06.inst.cfg @@ -0,0 +1,17 @@ +[general] +definition = ultimaker_s8 +name = CC+ 0.6 +version = 4 + +[metadata] +hardware_type = nozzle +setting_version = 25 +type = variant + +[values] +machine_nozzle_cool_down_speed = 0.9 +machine_nozzle_id = CC+ 0.6 +machine_nozzle_size = 0.6 +machine_nozzle_tip_outer_diameter = 1.2 +retraction_prime_speed = =retraction_speed +