Merge branch 'main' into DisplayInfoOnLCD

This commit is contained in:
HellAholic 2025-09-03 13:19:03 +02:00 committed by GitHub
commit bfefad0c94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
123 changed files with 3532 additions and 603 deletions

51
.github/workflows/find-packages.yml vendored Normal file
View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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"))

View file

@ -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)

46
cura/CuraRenderer.py Normal file
View file

@ -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

View file

@ -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": {

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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?

View file

@ -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()

View file

@ -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

View file

@ -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", "")

View file

@ -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:

View file

@ -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()

View file

@ -77,3 +77,4 @@ AppImage:
arch: {{ arch }}
file_name: {{ file_name }}
update-information: guess
comp: gzip

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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"<li>{extruder_name}</li>" for extruder_name in extruder_names]
warning_message = Message(
text=catalog.i18nc("@message", "<html>At least one extruder remains unused in this print:"
f"<ul><b>{"".join(unused_extruders)}</b></ul><br/>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.</html>"),
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

View file

@ -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()

View file

@ -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")
}
}
}
}
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")
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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()

View file

@ -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:

View file

@ -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")

View file

@ -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

View file

@ -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();
}
}
}
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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
}
}
}

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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
}

View file

@ -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

View file

@ -0,0 +1,8 @@
{
"name": "Paint Tools",
"author": "UltiMaker",
"version": "1.0.0",
"description": "Provides the paint tools.",
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -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.

View file

@ -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'}

View file

@ -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))

View file

@ -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

View file

@ -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;
}

View file

@ -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();

View file

@ -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();

View file

@ -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;
}

View file

@ -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()

View file

@ -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"]

View file

@ -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:

View file

@ -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

View file

@ -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
)

View file

@ -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"}})

View file

@ -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)

View file

@ -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.

View file

@ -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)

View file

@ -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 }
}
}

View file

@ -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 }
}
}

View file

@ -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": "<html>The ratio of retraction performed during the travel move, with the remainder completed while the nozzle is stationary, before traveling<ul><li>When 0, the entire retraction is performed while stationary, before the travel begins</li><li>When 100, the entire retraction is performed during the travel move, bypassing the stationary phase</li></ul></html>",
"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": "<html>The ratio of priming performed during the travel move, with the remainder completed while the nozzle is stationary, after traveling<ul><li>When 0, the entire priming is performed while stationary, after the travel ends</li><li>When 100, the entire priming is performed during the travel move, allowing the print to start immediately</li></ul></html>",
"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",

View file

@ -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 }
}
}

View file

@ -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" },

View file

@ -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'" }

View file

@ -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 }
}
}

View file

@ -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 }
}
}

View file

@ -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 }
}
}

View file

@ -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 }
}
}

View file

@ -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 }
}
}

View file

@ -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 }
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -5,6 +5,7 @@ version = 4
[metadata]
intent_category = engineering
is_experimental = True
material = generic_cpe_plus
quality_type = draft
setting_version = 25

View file

@ -5,6 +5,7 @@ version = 4
[metadata]
intent_category = engineering
is_experimental = True
material = generic_nylon-cf-slide
quality_type = draft
setting_version = 25

View file

@ -5,6 +5,7 @@ version = 4
[metadata]
intent_category = engineering
is_experimental = True
material = generic_pc
quality_type = draft
setting_version = 25

View file

@ -5,6 +5,7 @@ version = 4
[metadata]
intent_category = engineering
is_experimental = True
material = generic_petcf
quality_type = draft
setting_version = 25

View file

@ -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

View file

@ -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

Binary file not shown.

Binary file not shown.

View file

@ -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);
}
}
}

View file

@ -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

View file

@ -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"; }
}
}

View file

@ -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 : ""

View file

@ -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 != ""

View file

@ -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)
}
}
}

View file

@ -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

View file

@ -11,4 +11,5 @@ type = quality
weight = 0
[values]
layer_height = 0.1

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

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