mirror of
https://github.com/Ultimaker/Cura.git
synced 2026-02-16 01:19:37 -07:00
Merge branch 'main' into PP-658-Fix-BVT-temperature-limits-based-on-thermal-model-F4
Some checks failed
conan-package-resources / conan-package (push) Has been cancelled
conan-package / conan-package (push) Has been cancelled
printer-linter-format / Printer linter auto format (push) Has been cancelled
unit-test / Run unit tests (push) Has been cancelled
conan-package-resources / signal-curator (push) Has been cancelled
Some checks failed
conan-package-resources / conan-package (push) Has been cancelled
conan-package / conan-package (push) Has been cancelled
printer-linter-format / Printer linter auto format (push) Has been cancelled
unit-test / Run unit tests (push) Has been cancelled
conan-package-resources / signal-curator (push) Has been cancelled
This commit is contained in:
commit
40bde3008a
87 changed files with 3694 additions and 707 deletions
8
.github/workflows/find-packages.yml
vendored
8
.github/workflows/find-packages.yml
vendored
|
|
@ -1,14 +1,16 @@
|
|||
name: Find packages for Jira ticket and create installers
|
||||
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 for Conan package discovery (e.g., cura_12345)'
|
||||
description: 'Jira ticket number (e.g. CURA-15432 or cura_12345)'
|
||||
required: true
|
||||
type: string
|
||||
start_builds:
|
||||
default: false
|
||||
description: 'Start installers build based on found packages'
|
||||
default: true
|
||||
required: false
|
||||
type: boolean
|
||||
conan_args:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
20
conanfile.py
20
conanfile.py
|
|
@ -16,7 +16,7 @@ from conan import ConanFile
|
|||
from conan.tools.files import copy, rmdir, save, mkdir, rm, update_conandata
|
||||
from conan.tools.microsoft import unix_path
|
||||
from conan.tools.env import VirtualRunEnv, Environment, VirtualBuildEnv
|
||||
from conan.tools.scm import Version
|
||||
from conan.tools.scm import Version, Git
|
||||
from conan.errors import ConanInvalidConfiguration, ConanException
|
||||
|
||||
required_conan_version = ">=2.7.0" # When changing the version, also change the one in conandata.yml/extra_dependencies
|
||||
|
|
@ -329,10 +329,16 @@ class CuraConan(ConanFile):
|
|||
# If you want a specific Cura version to show up on the splash screen add the user configuration `user.cura:version=VERSION`
|
||||
# the global.conf, profile, package_info (of dependency) or via the cmd line `-c user.cura:version=VERSION`
|
||||
cura_version = Version(self.conf.get("user.cura:version", default = self.version, check_type = str))
|
||||
pre_tag = f"-{cura_version.pre}" if cura_version.pre else ""
|
||||
build_tag = f"+{cura_version.build}" if cura_version.build else ""
|
||||
internal_tag = f"+internal" if self.options.internal else ""
|
||||
cura_version = f"{cura_version.major}.{cura_version.minor}.{cura_version.patch}{pre_tag}{build_tag}{internal_tag}"
|
||||
extra_build_identifiers = []
|
||||
|
||||
if self.options.internal:
|
||||
extra_build_identifiers.append("internal")
|
||||
if str(cura_version.pre).startswith("alpha") and self.conan_data["commit"] != "unknown":
|
||||
extra_build_identifiers.append(self.conan_data["commit"][:6])
|
||||
|
||||
if extra_build_identifiers:
|
||||
separator = "+" if not cura_version.build else "."
|
||||
cura_version = Version(f"{cura_version}{separator}{'.'.join(extra_build_identifiers)}")
|
||||
|
||||
self.output.info(f"Write CuraVersion.py to {self.recipe_folder}")
|
||||
|
||||
|
|
@ -340,7 +346,7 @@ class CuraConan(ConanFile):
|
|||
f.write(cura_version_py.render(
|
||||
cura_app_name = self.name,
|
||||
cura_app_display_name = self._app_name,
|
||||
cura_version = cura_version,
|
||||
cura_version = str(cura_version),
|
||||
cura_version_full = self.version,
|
||||
cura_build_type = "Enterprise" if self.options.enterprise else "",
|
||||
cura_debug_mode = self.options.cura_debug_mode,
|
||||
|
|
@ -527,7 +533,7 @@ class CuraConan(ConanFile):
|
|||
))
|
||||
|
||||
def export(self):
|
||||
update_conandata(self, {"version": self.version})
|
||||
update_conandata(self, {"version": self.version, "commit": Git(self).get_commit()})
|
||||
|
||||
def export_sources(self):
|
||||
copy(self, "*", os.path.join(self.recipe_folder, "plugins"), os.path.join(self.export_sources_folder, "plugins"))
|
||||
|
|
|
|||
|
|
@ -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
46
cura/CuraRenderer.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -77,3 +77,4 @@ AppImage:
|
|||
arch: {{ arch }}
|
||||
file_name: {{ file_name }}
|
||||
update-information: guess
|
||||
comp: gzip
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
plugins/PaintTool/BrushColorButton.qml
Normal file
18
plugins/PaintTool/BrushColorButton.qml
Normal 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)
|
||||
}
|
||||
18
plugins/PaintTool/BrushShapeButton.qml
Normal file
18
plugins/PaintTool/BrushShapeButton.qml
Normal 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)
|
||||
}
|
||||
18
plugins/PaintTool/PaintModeButton.qml
Normal file
18
plugins/PaintTool/PaintModeButton.qml
Normal 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)
|
||||
}
|
||||
424
plugins/PaintTool/PaintTool.py
Normal file
424
plugins/PaintTool/PaintTool.py
Normal 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)
|
||||
301
plugins/PaintTool/PaintTool.qml
Normal file
301
plugins/PaintTool/PaintTool.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
104
plugins/PaintTool/PaintUndoCommand.py
Normal file
104
plugins/PaintTool/PaintUndoCommand.py
Normal 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
|
||||
173
plugins/PaintTool/PaintView.py
Normal file
173
plugins/PaintTool/PaintView.py
Normal 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)
|
||||
33
plugins/PaintTool/PrepareTextureJob.py
Normal file
33
plugins/PaintTool/PrepareTextureJob.py
Normal 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)
|
||||
|
||||
35
plugins/PaintTool/__init__.py
Normal file
35
plugins/PaintTool/__init__.py
Normal 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
|
||||
}
|
||||
146
plugins/PaintTool/paint.shader
Normal file
146
plugins/PaintTool/paint.shader
Normal 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
|
||||
8
plugins/PaintTool/plugin.json
Normal file
8
plugins/PaintTool/plugin.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "Paint Tools",
|
||||
"author": "UltiMaker",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides the paint tools.",
|
||||
"api": 8,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
571
plugins/PostProcessingPlugin/scripts/AnnealingOrDrying.py
Normal file
571
plugins/PostProcessingPlugin/scripts/AnnealingOrDrying.py
Normal file
|
|
@ -0,0 +1,571 @@
|
|||
"""
|
||||
Copyright (c) 2025 GregValiant (Greg Foresi)
|
||||
|
||||
When Annealing:
|
||||
The user may elect to hold the build plate at a temperature for a period of time. When the hold expires, the 'Timed Cooldown' will begin.
|
||||
If there is no 'Hold Time' then the 'Annealing' cooldown will begin when the print ends. In 'Annealing' the bed temperature drops in 3° increments across the time span.
|
||||
G4 commands are used for the cooldown steps.
|
||||
If there is a 'Heated Chamber' then the chamber will start to cool when the bed temperature reaches the chamber temperature.
|
||||
|
||||
When drying filament:
|
||||
The bed must be empty because the printer will auto-home before raising the Z to 'machine_height minus 20mm' and then park the head in the XY.
|
||||
The bed will heat up to the set point.
|
||||
G4 commands are used to keep the machine from turning the bed off until the Drying Time has expired.
|
||||
If you happen to have an enclosure with a fan, the fan can be set up to run during the drying or annealing.
|
||||
|
||||
NOTE: This script uses the G4 Dwell command as a timer. It cannot be canceled from the LCD. If you wish to 'escape' from G4 you might have to cancel the print from the LCD or cycle the printer on and off to reset.
|
||||
"""
|
||||
|
||||
from UM.Application import Application
|
||||
from ..Script import Script
|
||||
from UM.Message import Message
|
||||
|
||||
class AnnealingOrDrying(Script):
|
||||
|
||||
def initialize(self) -> None:
|
||||
super().initialize()
|
||||
# Get the Bed Temperature from Cura
|
||||
self.global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
bed_temp_during_print = str(self.global_stack.getProperty("material_bed_temperature", "value"))
|
||||
self._instance.setProperty("startout_temp", "value", bed_temp_during_print)
|
||||
# Get the Build Volume temperature if there is one
|
||||
heated_build_volume = bool(self.global_stack.getProperty("machine_heated_build_volume", "value"))
|
||||
chamber_fan_nr = self.global_stack.getProperty("build_volume_fan_nr", "value")
|
||||
extruder_count = self.global_stack.getProperty("machine_extruder_count", "value")
|
||||
if heated_build_volume:
|
||||
chamber_temp = self.global_stack.getProperty("build_volume_temperature", "value")
|
||||
self._instance.setProperty("has_build_volume_heater", "value", heated_build_volume)
|
||||
self._instance.setProperty("build_volume_temp", "value", chamber_temp)
|
||||
try:
|
||||
if chamber_fan_nr > 0:
|
||||
self._instance.setProperty("enable_chamber_fan_setting", "value", True)
|
||||
except:
|
||||
pass
|
||||
|
||||
def getSettingDataString(self):
|
||||
return """{
|
||||
"name": "Annealing CoolDown or Filament Drying",
|
||||
"key": "AnnealingOrDrying",
|
||||
"metadata": {},
|
||||
"version": 2,
|
||||
"settings":
|
||||
{
|
||||
"enable_script":
|
||||
{
|
||||
"label": "Enable the Script",
|
||||
"description": "If it isn't enabled it doesn't run.",
|
||||
"type": "bool",
|
||||
"default_value": true,
|
||||
"enabled": true
|
||||
},
|
||||
"cycle_type":
|
||||
{
|
||||
"label": "Anneal Print or Dry Filament",
|
||||
"description": "Whether to Anneal the Print (by keeping the bed hot for a period of time), or to use the bed as a Filament Dryer. If drying; you will still need to slice a model, but it will not print. The gcode will consist only of a short script to heat the bed, wait for a while, then turn the bed off. The 'Z' will move to the max height and XY park position so the filament can be covered. The 'Hold Time', 'Bed Start Temp' and (if applicable) the 'Chamber Temp' come from these settings rather than from the Cura settings. When annealing; the Timed Cooldown will commence when the print ends.",
|
||||
"type": "enum",
|
||||
"options":
|
||||
{
|
||||
"anneal_cycle": "Anneal Print",
|
||||
"dry_cycle": "Dry Filament"
|
||||
},
|
||||
"default_value": "anneal_cycle",
|
||||
"enabled": true,
|
||||
"enabled": "enable_script"
|
||||
},
|
||||
"heating_zone_selection":
|
||||
{
|
||||
"label": "Hold the Temp for the:",
|
||||
"description": "Select the 'Bed' for just the bed, or 'Bed and Chamber' if you want to include your 'Heated Build Volume'.",
|
||||
"type": "enum",
|
||||
"options":
|
||||
{
|
||||
"bed_only": "Bed",
|
||||
"bed_chamber": "Bed and Chamber"
|
||||
},
|
||||
"default_value": "bed_only",
|
||||
"enabled": "enable_script"
|
||||
},
|
||||
"wait_time":
|
||||
{
|
||||
"label": "Hold Time at Temp(s)",
|
||||
"description": "Hold the bed temp at the 'Bed Start Out Temperature' for this amount of time (in decimal hours). When this time expires then the Annealing cool down will start. This is also the 'Drying Time' used when 'Drying Filament'.",
|
||||
"type": "float",
|
||||
"default_value": 0.0,
|
||||
"unit": "Decimal Hrs ",
|
||||
"enabled": "enable_script and cycle_type == 'anneal_cycle'"
|
||||
},
|
||||
"dry_time":
|
||||
{
|
||||
"label": "Drying Time",
|
||||
"description": "Hold the bed temp at the 'Bed Start Out Temperature' for this amount of time (in decimal hours). When this time expires the bed will shut off.",
|
||||
"type": "float",
|
||||
"default_value": 4.0,
|
||||
"unit": "Decimal Hrs ",
|
||||
"enabled": "enable_script and cycle_type == 'dry_cycle'"
|
||||
},
|
||||
"pause_cmd":
|
||||
{
|
||||
"label": "Pause Cmd for Auto-Home",
|
||||
"description": "Not required when you are paying attention and the bed is empty; ELSE; Enter the pause command to use prior to the Auto-Home command. The pause insures that the user IS paying attention and clears the build plate for Auto-Home. If you leave the box empty then there won't be a pause.",
|
||||
"type": "str",
|
||||
"default_value": "",
|
||||
"enabled": "enable_script and cycle_type == 'dry_cycle'"
|
||||
},
|
||||
"startout_temp":
|
||||
{
|
||||
"label": "Bed Start Out Temperature:",
|
||||
"description": "Enter the temperature to start at. This is typically the bed temperature during the print but can be changed here. This is also the temperature used when drying filament.",
|
||||
"type": "int",
|
||||
"value": 30,
|
||||
"unit": "Degrees ",
|
||||
"minimum_value": 30,
|
||||
"maximum_value": 110,
|
||||
"maximum_value_warning": 100,
|
||||
"enabled": "enable_script"
|
||||
},
|
||||
"lowest_temp":
|
||||
{
|
||||
"label": "Shut-Off Temp:",
|
||||
"description": "Enter the lowest temperature to control the cool down. This is the shut-off temperature for the build plate and (when applicable) the Heated Chamber. The minimum value is 30",
|
||||
"type": "int",
|
||||
"default_value": 30,
|
||||
"unit": "Degrees ",
|
||||
"minimum_value": 30,
|
||||
"enabled": "enable_script and cycle_type == 'anneal_cycle'"
|
||||
},
|
||||
"build_volume_temp":
|
||||
{
|
||||
"label": "Build Volume Temperature:",
|
||||
"description": "Enter the temperature for the Build Volume (Heated Chamber). This is typically the temperature during the print but can be changed here.",
|
||||
"type": "int",
|
||||
"value": 24,
|
||||
"unit": "Degrees ",
|
||||
"minimum_value": 0,
|
||||
"maximum_value": 90,
|
||||
"maximum_value_warning": 75,
|
||||
"enabled": "enable_script and has_build_volume_heater and heating_zone_selection == 'bed_chamber'"
|
||||
},
|
||||
"enable_chamber_fan_setting":
|
||||
{
|
||||
"label": "Hidden Setting",
|
||||
"description": "Enables chamber fan and speed.",
|
||||
"type": "bool",
|
||||
"default_value": false,
|
||||
"enabled": false
|
||||
},
|
||||
"chamber_fan_speed":
|
||||
{
|
||||
"label": "Chamber Fan Speed",
|
||||
"description": "Set to % fan speed. Set to 0 to turn it off.",
|
||||
"type": "int",
|
||||
"default_value": 0,
|
||||
"minimum_value": 0,
|
||||
"maximum_value": 100,
|
||||
"unit": "% ",
|
||||
"enabled": "enable_script and enable_chamber_fan_setting"
|
||||
},
|
||||
"time_span":
|
||||
{
|
||||
"label": "Cool Down Time Span:",
|
||||
"description": "The total amount of time (in decimal hours) to control the cool down. The build plate temperature will be dropped in 3° increments across this time span. 'Cool Down Time' starts at the end of the 'Hold Time' if you entered one.",
|
||||
"type": "float",
|
||||
"default_value": 1.0,
|
||||
"unit": "Decimal Hrs ",
|
||||
"minimum_value_warning": 0.25,
|
||||
"enabled": "enable_script and cycle_type == 'anneal_cycle'"
|
||||
},
|
||||
"park_head":
|
||||
{
|
||||
"label": "Park at MaxX and MaxY",
|
||||
"description": "When unchecked, the park position is X0 Y0. Enable this setting to move the nozzle to the Max X and Max Y to allow access to the print.",
|
||||
"type": "bool",
|
||||
"default_value": false,
|
||||
"enabled": "enable_script and cycle_type == 'anneal_cycle'"
|
||||
},
|
||||
"park_max_z":
|
||||
{
|
||||
"label": "Move to MaxZ",
|
||||
"description": "Enable this setting to move the nozzle to 'Machine_Height - 20' to allow the print to be covered.",
|
||||
"type": "bool",
|
||||
"default_value": false,
|
||||
"enabled": "enable_script and cycle_type == 'anneal_cycle'"
|
||||
},
|
||||
"beep_when_done":
|
||||
{
|
||||
"label": "Beep when done",
|
||||
"description": "Add an annoying noise when the Cool Down completes.",
|
||||
"type": "bool",
|
||||
"default_value": true,
|
||||
"enabled": "enable_script"
|
||||
},
|
||||
"beep_duration":
|
||||
{
|
||||
"label": "Beep Duration",
|
||||
"description": "The length of the buzzer sound. Units are in milliseconds so 1000ms = 1 second.",
|
||||
"type": "int",
|
||||
"unit": "milliseconds ",
|
||||
"default_value": 1000,
|
||||
"enabled": "beep_when_done and enable_script"
|
||||
},
|
||||
"add_messages":
|
||||
{
|
||||
"label": "Include M117 and M118 messages",
|
||||
"description": "Add messages to the LCD and any print server.",
|
||||
"type": "bool",
|
||||
"default_value": false,
|
||||
"enabled": "enable_script"
|
||||
},
|
||||
"has_build_volume_heater":
|
||||
{
|
||||
"label": "Hidden setting",
|
||||
"description": "Hidden. This setting enables the build volume settings.",
|
||||
"type": "bool",
|
||||
"default_value": false,
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
def execute(self, data):
|
||||
# Exit if there is no heated bed.
|
||||
if not bool(self.global_stack.getProperty("machine_heated_bed", "value")):
|
||||
Message(title = "[Anneal or Dry Filament]", text = "The script did not run because Heated Bed is disabled in Machine Settings.").show()
|
||||
return data
|
||||
# Enter a message in the gcode if the script is not enabled.
|
||||
if not bool(self.getSettingValueByKey("enable_script")):
|
||||
data[0] += "; [Anneal or Dry Filament] was not enabled\n"
|
||||
return data
|
||||
lowest_temp = int(self.getSettingValueByKey("lowest_temp"))
|
||||
|
||||
# If the shutoff temp is under 30° then exit as a safety precaution so the bed doesn't stay on.
|
||||
if lowest_temp < 30:
|
||||
data[0] += "; Anneal or Dry Filament did not run. Shutoff Temp < 30\n"
|
||||
Message(title = "[Anneal or Dry Filament]", text = "The script did not run because the Shutoff Temp is less than 30°.").show()
|
||||
return data
|
||||
extruders = self.global_stack.extruderList
|
||||
bed_temperature = int(self.getSettingValueByKey("startout_temp"))
|
||||
heated_chamber = bool(self.global_stack.getProperty("machine_heated_build_volume", "value"))
|
||||
heating_zone = self.getSettingValueByKey("heating_zone_selection")
|
||||
|
||||
# Get the heated chamber temperature or set to 0 if no chamber
|
||||
if heated_chamber:
|
||||
chamber_temp = str(self.getSettingValueByKey("build_volume_temp"))
|
||||
else:
|
||||
heating_zone = "bed_only"
|
||||
chamber_temp = "0"
|
||||
|
||||
# Beep line
|
||||
if bool(self.getSettingValueByKey("beep_when_done")):
|
||||
beep_duration = self.getSettingValueByKey("beep_duration")
|
||||
self.beep_string = f"M300 S440 P{beep_duration} ; Beep\n"
|
||||
else:
|
||||
self.beep_string = ""
|
||||
|
||||
# For compatibility with earlier Cura versions
|
||||
if self.global_stack.getProperty("build_volume_fan_nr", "value") is not None:
|
||||
has_bv_fan = bool(self.global_stack.getProperty("build_volume_fan_nr", "value"))
|
||||
bv_fan_nr = int(self.global_stack.getProperty("build_volume_fan_nr", "value"))
|
||||
if bv_fan_nr > 0:
|
||||
speed_bv_fan = int(self.getSettingValueByKey("chamber_fan_speed"))
|
||||
else:
|
||||
speed_bv_fan = 0
|
||||
|
||||
if bool(extruders[0].getProperty("machine_scale_fan_speed_zero_to_one", "value")) and has_bv_fan:
|
||||
speed_bv_fan = round(speed_bv_fan * 0.01)
|
||||
else:
|
||||
speed_bv_fan = round(speed_bv_fan * 2.55)
|
||||
|
||||
if has_bv_fan and speed_bv_fan > 0:
|
||||
self.bv_fan_on_str = f"M106 S{speed_bv_fan} P{bv_fan_nr} ; Build Chamber Fan On\n"
|
||||
self.bv_fan_off_str = f"M106 S0 P{bv_fan_nr} ; Build Chamber Fan Off\n"
|
||||
else:
|
||||
self.bv_fan_on_str = ""
|
||||
self.bv_fan_off_str = ""
|
||||
else:
|
||||
has_bv_fan = False
|
||||
bv_fan_nr = 0
|
||||
speed_bv_fan = 0
|
||||
self.bv_fan_on_str = ""
|
||||
self.bv_fan_off_str = ""
|
||||
|
||||
# Park Head
|
||||
max_y = str(self.global_stack.getProperty("machine_depth", "value"))
|
||||
max_x = str(self.global_stack.getProperty("machine_width", "value"))
|
||||
|
||||
# Max_z is limited to 'machine_height - 20' just so the print head doesn't smack into anything.
|
||||
max_z = str(int(self.global_stack.getProperty("machine_height", "value")) - 20)
|
||||
speed_travel = str(round(extruders[0].getProperty("speed_travel", "value")*60))
|
||||
park_xy = bool(self.getSettingValueByKey("park_head"))
|
||||
park_z = bool(self.getSettingValueByKey("park_max_z"))
|
||||
cycle_type = self.getSettingValueByKey("cycle_type")
|
||||
add_messages = bool(self.getSettingValueByKey("add_messages"))
|
||||
|
||||
if cycle_type == "anneal_cycle":
|
||||
data = self._anneal_print(add_messages, data, bed_temperature, chamber_temp, heated_chamber, heating_zone, lowest_temp, max_x, max_y, max_z, park_xy, park_z, speed_travel)
|
||||
elif cycle_type == "dry_cycle":
|
||||
data = self._dry_filament_only(data, bed_temperature, chamber_temp, heated_chamber, heating_zone, max_y, max_z, speed_travel)
|
||||
|
||||
return data
|
||||
|
||||
def _anneal_print(
|
||||
self,
|
||||
add_messages: bool,
|
||||
anneal_data: str,
|
||||
bed_temperature: int,
|
||||
chamber_temp: str,
|
||||
heated_chamber: bool,
|
||||
heating_zone: str,
|
||||
lowest_temp: int,
|
||||
max_x: str,
|
||||
max_y: str,
|
||||
max_z: str,
|
||||
park_xy: bool,
|
||||
park_z: bool,
|
||||
speed_travel: str) -> str:
|
||||
"""
|
||||
The procedure disables the M140 (and M141) lines at the end of the print, and adds additional bed (and chamber) temperature commands to the end of the G-Code file.
|
||||
The bed is allowed to cool down over a period of time.
|
||||
|
||||
:param add_messages: Whether to include M117 and M118 messages for LCD and print server
|
||||
:param anneal_data: The G-code data to be modified with annealing commands
|
||||
:param bed_temperature: Starting bed temperature in degrees Celsius
|
||||
:param chamber_temp: Chamber/build volume temperature in degrees Celsius as string
|
||||
:param heated_chamber: Whether the printer has a heated build volume/chamber
|
||||
:param heating_zone: Zone selection - "bed_only" or "bed_chamber"
|
||||
:param lowest_temp: Final shutdown temperature in degrees Celsius
|
||||
:param max_x: Maximum X axis position for parking as string
|
||||
:param max_y: Maximum Y axis position for parking as string
|
||||
:param max_z: Maximum Z axis position (machine height - 20mm) as string
|
||||
:param park_xy: Whether to park the print head at max X and Y positions
|
||||
:param park_z: Whether to raise Z to maximum safe height
|
||||
:param speed_travel: Travel speed for positioning moves in mm/min as string
|
||||
:return: Modified G-code data with annealing cooldown sequence
|
||||
"""
|
||||
# Put the head parking string together
|
||||
bed_temp_during_print = int(self.global_stack.getProperty("material_bed_temperature", "value"))
|
||||
time_minutes = 1
|
||||
time_span = int(float(self.getSettingValueByKey("time_span")) * 3600)
|
||||
park_string = ""
|
||||
if park_xy:
|
||||
park_string += f"G0 F{speed_travel} X{max_x} Y{max_y} ; Park XY\n"
|
||||
if park_z:
|
||||
park_string += f"G0 Z{max_z} ; Raise Z to 'ZMax - 20'\n"
|
||||
if not park_xy and not park_z:
|
||||
park_string += f"G91 ; Relative movement\nG0 F{speed_travel} Z5 ; Raise Z\nG90 ; Absolute movement\nG0 X0 Y0 ; Park\n"
|
||||
park_string += "M84 X Y E ; Disable steppers except Z\n"
|
||||
|
||||
# Calculate the temperature differential
|
||||
hysteresis = bed_temperature - lowest_temp
|
||||
|
||||
# Exit if the bed temp is below the shutoff temp
|
||||
if hysteresis <= 0:
|
||||
anneal_data[0] += "; Anneal or Dry Filament did not run. Bed Temp < Shutoff Temp\n"
|
||||
Message(title = "Anneal or Dry Filament", text = "Did not run because the Bed Temp < Shutoff Temp.").show()
|
||||
return anneal_data
|
||||
|
||||
# Drop the bed temperature in 3° increments.
|
||||
num_steps = int(hysteresis / 3)
|
||||
step_index = 2
|
||||
deg_per_step = int(hysteresis / num_steps)
|
||||
time_per_step = int(time_span / num_steps)
|
||||
step_down = bed_temperature
|
||||
wait_time = int(float(self.getSettingValueByKey("wait_time")) * 3600)
|
||||
|
||||
# Put the first lines of the anneal string together
|
||||
anneal_string = ";\n;TYPE:CUSTOM ---------------- Anneal Print\n"
|
||||
if bed_temperature == bed_temp_during_print:
|
||||
anneal_string += self.beep_string
|
||||
if add_messages:
|
||||
anneal_string += "M117 Cool Down for " + str(round((wait_time + time_span)/3600,2)) + "hr\n"
|
||||
anneal_string += "M118 Cool Down for " + str(round((wait_time + time_span)/3600,2)) + "hr\n"
|
||||
anneal_string += self.bv_fan_on_str
|
||||
if wait_time > 0:
|
||||
# Add the parking string BEFORE the M190
|
||||
anneal_string += park_string
|
||||
if heating_zone == "bed_only":
|
||||
anneal_string += f"M190 S{bed_temperature} ; Set the bed temp\n{self.beep_string}"
|
||||
if heating_zone == "bed_chamber":
|
||||
anneal_string += f"M190 S{bed_temperature} ; Set the bed temp\nM141 S{chamber_temp} ; Set the chamber temp\n{self.beep_string}"
|
||||
anneal_string += f"G4 S{wait_time} ; Hold for {round(wait_time / 3600,2)} hrs\n"
|
||||
else:
|
||||
# Add the parking string AFTER the M140
|
||||
anneal_string += f"M140 S{step_down} ; Set bed temp\n"
|
||||
anneal_string += park_string
|
||||
anneal_string += f"G4 S{time_per_step} ; wait time in seconds\n"
|
||||
|
||||
step_down -= deg_per_step
|
||||
time_remaining = round(time_span/3600,2)
|
||||
|
||||
# Step the bed/chamber temps down and add each step to the anneal string. The chamber remains at it's temperature until the bed gets down to that temperature.
|
||||
for num in range(bed_temperature, lowest_temp, -3):
|
||||
anneal_string += f"M140 S{step_down} ; Step down bed\n"
|
||||
if heating_zone == "bed_chamber" and int(step_down) < int(chamber_temp):
|
||||
anneal_string += f"M141 S{step_down} ; Step down chamber\n"
|
||||
anneal_string += f"G4 S{time_per_step} ; Wait\n"
|
||||
if time_remaining >= 1.00:
|
||||
if add_messages:
|
||||
anneal_string += f"M117 CoolDown - {round(time_remaining,1)}hr\n"
|
||||
anneal_string += f"M118 CoolDown - {round(time_remaining,1)}hr\n"
|
||||
elif time_minutes > 0:
|
||||
time_minutes = round(time_remaining * 60,1)
|
||||
if add_messages:
|
||||
anneal_string += f"M117 CoolDown - {time_minutes}min\n"
|
||||
anneal_string += f"M118 CoolDown - {time_minutes}min\n"
|
||||
time_remaining = round((time_span-(step_index*time_per_step))/3600,2)
|
||||
step_down -= deg_per_step
|
||||
step_index += 1
|
||||
if step_down <= lowest_temp:
|
||||
break
|
||||
|
||||
# Close out the anneal string
|
||||
anneal_string += "M140 S0 ; Shut off the bed heater" + "\n"
|
||||
if heating_zone == "bed_chamber":
|
||||
anneal_string += "M141 S0 ; Shut off the chamber heater\n"
|
||||
anneal_string += self.bv_fan_off_str
|
||||
anneal_string += self.beep_string
|
||||
if add_messages:
|
||||
anneal_string += "M117 CoolDown Complete\n"
|
||||
anneal_string += "M118 CoolDown Complete\n"
|
||||
anneal_string += ";TYPE:CUSTOM ---------------- End of Anneal\n;"
|
||||
|
||||
# Format the inserted lines.
|
||||
anneal_lines = anneal_string.split("\n")
|
||||
for index, line in enumerate(anneal_lines):
|
||||
if not line.startswith(";") and ";" in line:
|
||||
front_txt = anneal_lines[index].split(";")[0]
|
||||
back_txt = anneal_lines[index].split(";")[1]
|
||||
anneal_lines[index] = front_txt + str(" " * (30 - len(front_txt))) +";" + back_txt
|
||||
anneal_string = "\n".join(anneal_lines) + "\n"
|
||||
|
||||
end_gcode = anneal_data[-1]
|
||||
end_lines = end_gcode.split("\n")
|
||||
|
||||
# Comment out the existing M140 S0 lines in the ending gcode.
|
||||
for num in range(len(end_lines)-1,-1,-1):
|
||||
if end_lines[num].startswith("M140 S0"):
|
||||
end_lines[num] = ";M140 S0 ; Shutoff Overide - Anneal or Dry Filament"
|
||||
anneal_data[-1] = "\n".join(end_lines)
|
||||
|
||||
# If there is a Heated Chamber and it's included then comment out the M141 S0 line
|
||||
if heating_zone == "bed_chamber" and heated_chamber:
|
||||
for num in range(0,len(end_lines)-1):
|
||||
if end_lines[num].startswith("M141 S0"):
|
||||
end_lines[num] = ";M141 S0 ; Shutoff Overide - Anneal or Dry Filament"
|
||||
anneal_data[-1] = "\n".join(end_lines)
|
||||
|
||||
# If park head is enabled then dont let the steppers disable until the head is parked
|
||||
disable_string = ""
|
||||
for num in range(0,len(end_lines)-1):
|
||||
if end_lines[num][:3] in ("M84", "M18"):
|
||||
disable_string = end_lines[num] + "\n"
|
||||
stepper_timeout = int(wait_time + time_span)
|
||||
if stepper_timeout > 14400: stepper_timeout = 14400
|
||||
end_lines[num] = ";" + end_lines[num] + " ; Overide - Anneal or Dry Filament"
|
||||
end_lines.insert(num, "M84 S" + str(stepper_timeout) + " ; Increase stepper timeout - Anneal or Dry Filament")
|
||||
anneal_data[-1] = "\n".join(end_lines)
|
||||
break
|
||||
|
||||
# The Anneal string is the new end of the gcode so move the 'End of Gcode' comment line in case there are other scripts running
|
||||
anneal_data[-1] = anneal_data[-1].replace(";End of Gcode", anneal_string + disable_string + ";End of Gcode")
|
||||
return anneal_data
|
||||
|
||||
def _dry_filament_only(
|
||||
self,
|
||||
bed_temperature: int,
|
||||
chamber_temp: int,
|
||||
drydata: str,
|
||||
heated_chamber: bool,
|
||||
heating_zone: str,
|
||||
max_y: str,
|
||||
max_z: str,
|
||||
speed_travel: str) -> str:
|
||||
"""
|
||||
This procedure turns the bed on, homes the printer, parks the head. After the time period the bed is turned off.
|
||||
There is no actual print in the generated gcode, just a couple of moves to get the nozzle out of the way, and the bed heat (and possibly chamber heat) control.
|
||||
It allows a user to use the bed to warm up and hopefully dry a filament roll.
|
||||
|
||||
:param bed_temperature: Bed temperature for drying in degrees Celsius
|
||||
:param chamber_temp: Chamber/build volume temperature for drying in degrees Celsius
|
||||
:param drydata: The G-code data to be replaced with filament drying commands
|
||||
:param heated_chamber: Whether the printer has a heated build volume/chamber
|
||||
:param heating_zone: Zone selection - "bed_only" or "bed_chamber"
|
||||
:param max_y: Maximum Y axis position for parking as string
|
||||
:param max_z: Maximum Z axis position (machine height - 20mm) as string
|
||||
:param speed_travel: Travel speed for positioning moves in mm/min as string
|
||||
:return: Modified G-code data containing only filament drying sequence
|
||||
"""
|
||||
for num in range(2, len(drydata)):
|
||||
drydata[num] = ""
|
||||
drydata[0] = drydata[0].split("\n")[0] + "\n"
|
||||
add_messages = bool(self.getSettingValueByKey("add_messages"))
|
||||
pause_cmd = self.getSettingValueByKey("pause_cmd")
|
||||
if pause_cmd != "":
|
||||
pause_cmd = self.beep_string + pause_cmd
|
||||
dry_time = self.getSettingValueByKey("dry_time") * 3600
|
||||
lines = drydata[1].split("\n")
|
||||
drying_string = lines[0] + f"\n;............TYPE:CUSTOM: Dry Filament\n{self.beep_string}"
|
||||
if add_messages:
|
||||
drying_string += f"M117 Cool Down for {round(dry_time/3600,2)} hr ; Message\n"
|
||||
drying_string += f"M118 Cool Down for {round(dry_time/3600,2)} hr ; Message\n"
|
||||
|
||||
# M113 sends messages to a print server as a 'Keep Alive' and can generate a lot of traffic over the USB
|
||||
drying_string += "M113 S0 ; No echo\n"
|
||||
drying_string += f"M84 S{round(dry_time)} ; Set stepper timeout\n"
|
||||
drying_string += f"M140 S{bed_temperature} ; Heat bed\n"
|
||||
drying_string += self.bv_fan_on_str
|
||||
if heated_chamber and heating_zone == "bed_chamber":
|
||||
drying_string += f"M141 S{chamber_temp} ; Chamber temp\n"
|
||||
if pause_cmd == "M0":
|
||||
pause_cmd = "M0 Clear bed and click...; Pause"
|
||||
if pause_cmd != "":
|
||||
drying_string += pause_cmd + " ; Pause\n"
|
||||
drying_string += "G28 ; Auto-Home\n"
|
||||
drying_string += f"G0 F{speed_travel} Z{max_z} ; Raise Z to 'ZMax - 20'\n"
|
||||
drying_string += f"G0 F{speed_travel} X0 Y{max_y} ; Park print head\n"
|
||||
if dry_time <= 3600:
|
||||
if add_messages:
|
||||
drying_string += f"M117 {dry_time/3600} hr remaining ; Message\n"
|
||||
drying_string += f"M118 {dry_time/3600} hr remaining ; Message\n"
|
||||
drying_string += f"G4 S{dry_time} ; Dry time\n"
|
||||
elif dry_time > 3600:
|
||||
temp_time = dry_time
|
||||
while temp_time > 3600:
|
||||
if add_messages:
|
||||
drying_string += f"M117 {temp_time/3600} hr remaining ; Message\n"
|
||||
drying_string += f"M118 {temp_time/3600} hr remaining ; Message\n"
|
||||
drying_string += f"G4 S3600 ; Dry time split\n"
|
||||
if temp_time > 3600:
|
||||
temp_time -= 3600
|
||||
if temp_time > 0:
|
||||
if add_messages:
|
||||
drying_string += f"M117 {temp_time/3600} hr remaining ; Message\n"
|
||||
drying_string += f"M118 {temp_time/3600} hr remaining ; Message\n"
|
||||
drying_string += f"G4 S{temp_time} ; Dry time\n"
|
||||
if heated_chamber and heating_zone == "bed_chamber":
|
||||
drying_string += f"M141 S0 ; Shut off chamber\n"
|
||||
drying_string += "M140 S0 ; Shut off bed\n"
|
||||
drying_string += self.bv_fan_off_str
|
||||
if self.getSettingValueByKey("beep_when_done"):
|
||||
beep_duration = self.getSettingValueByKey("beep_duration")
|
||||
drying_string += self.beep_string
|
||||
if add_messages:
|
||||
drying_string += "M117 End of drying cycle ; Message\n"
|
||||
drying_string += "M118 End of drying cycle ; Message\n"
|
||||
drying_string += "M84 X Y E ; Disable steppers except Z\n"
|
||||
drying_string += ";End of Gcode"
|
||||
|
||||
# Format the lines
|
||||
lines = drying_string.split("\n")
|
||||
for index, line in enumerate(lines):
|
||||
if not line.startswith(";") and ";" in line:
|
||||
front_txt = lines[index].split(";")[0]
|
||||
back_txt = lines[index].split(";")[1]
|
||||
lines[index] = front_txt + str(" " * (30 - len(front_txt))) +";" + back_txt
|
||||
drydata[1] = "\n".join(lines) + "\n"
|
||||
dry_txt = "; Drying time ...................... " + str(self.getSettingValueByKey("dry_time")) + " hrs\n"
|
||||
dry_txt += "; Drying temperature ........ " + str(bed_temperature) + "°\n"
|
||||
if heated_chamber and heating_zone == "bed_chamber":
|
||||
dry_txt += "; Chamber temperature ... " + str(chamber_temp) + "°\n"
|
||||
Message(title = "[Dry Filament]", text = dry_txt).show()
|
||||
drydata[0] = "; <<< This is a filament drying file only. There is no actual print. >>>\n;\n" + dry_txt + ";\n"
|
||||
return drydata
|
||||
|
|
@ -1,31 +1,36 @@
|
|||
# Display Filename and Layer on the LCD by Amanda de Castilho on August 28, 2018
|
||||
# Modified: Joshua Pope-Lewis on November 16, 2018
|
||||
# Display Progress on LCD by Mathias Lyngklip Kjeldgaard, Alexander Gee, Kimmo Toivanen, Inigo Martinez on July 31, 2019
|
||||
# Show Progress was adapted from Display Progress by Louis Wooters on January 6, 2020. His changes are included here.
|
||||
#---------------------------------------------------------------
|
||||
# DisplayNameOrProgressOnLCD.py
|
||||
# Cura Post-Process plugin
|
||||
# Combines 'Display Filename and Layer on the LCD' with 'Display Progress'
|
||||
# Combined and with additions by: GregValiant (Greg Foresi)
|
||||
# Date: September 8, 2023
|
||||
# NOTE: This combined post processor will make 'Display Filename and Layer on the LCD' and 'Display Progress' obsolete
|
||||
# Description: Display Filename and Layer options:
|
||||
# Status messages sent to the printer...
|
||||
# - Scrolling (SCROLL_LONG_FILENAMES) if enabled in Marlin and you aren't printing a small item select this option.
|
||||
# - Name: By default it will use the name generated by Cura (EG: TT_Test_Cube) - You may enter a custom name here
|
||||
# - Start Num: Choose which number you prefer for the initial layer, 0 or 1
|
||||
# - Max Layer: Enabling this will show how many layers are in the entire print (EG: Layer 1 of 265!)
|
||||
# - Add prefix 'Printing': Enabling this will add the prefix 'Printing'
|
||||
# - Example Line on LCD: Printing Layer 0 of 395 3DBenchy
|
||||
# Display Progress options:
|
||||
# - Display Total Layer Count
|
||||
# - Disply Time Remaining for the print
|
||||
# - Time Fudge Factor % - Divide the Actual Print Time by the Cura Estimate. Enter as a percentage and the displayed time will be adjusted. This allows you to bring the displayed time closer to reality (Ex: Entering 87.5 would indicate an adjustment to 87.5% of the Cura estimate).
|
||||
# - Example line on LCD: 1/479 | ET 2h13m
|
||||
# - Time to Pauses changes the M117/M118 lines to countdown to the next pause as 1/479 | TP 2h36m
|
||||
# - 'Add M118 Line' is available with either option. M118 will bounce the message back to a remote print server through the USB connection.
|
||||
# - 'Add M73 Line' is used by 'Display Progress' only. There are options to incluse M73 P(percent) and M73 R(time remaining)
|
||||
# - Enable 'Finish-Time' Message - when enabled, takes the Print Time and calculates when the print will end. It takes into account the Time Fudge Factor. The user may enter a print start time. This is also available for Display Filename.
|
||||
"""
|
||||
Display Filename and Layer on the LCD by Amanda de Castilho on August 28, 2018
|
||||
Modified: Joshua Pope-Lewis on November 16, 2018
|
||||
Display Progress on LCD by Mathias Lyngklip Kjeldgaard, Alexander Gee, Kimmo Toivanen, Inigo Martinez on July 31, 2019
|
||||
Show Progress was adapted from Display Progress by Louis Wooters on January 6, 2020. His changes are included here.
|
||||
---------------------------------------------------------------
|
||||
DisplayNameOrProgressOnLCD.py
|
||||
Cura Post-Process plugin
|
||||
Combines 'Display Filename and Layer on the LCD' with 'Display Progress'
|
||||
Combined and with additions by: GregValiant (Greg Foresi)
|
||||
Date: September 8, 2023
|
||||
Date: March 31, 2024 - Bug fix for problem with adding M118 lines if 'Remaining Time' was not checked.
|
||||
NOTE: This combined post processor will make 'Display Filename and Layer on the LCD' and 'Display Progress' obsolete
|
||||
Description: Display Filename and Layer options:
|
||||
Status messages sent to the printer...
|
||||
- Scrolling (SCROLL_LONG_FILENAMES) if enabled in Marlin and you aren't printing a small item select this option.
|
||||
- Name: By default it will use the name generated by Cura (EG: TT_Test_Cube) - You may enter a custom name here
|
||||
- Start Num: Choose which number you prefer for the initial layer, 0 or 1
|
||||
- Max Layer: Enabling this will show how many layers are in the entire print (EG: Layer 1 of 265!)
|
||||
- Add prefix 'Printing': Enabling this will add the prefix 'Printing'
|
||||
- Example Line on LCD: Printing Layer 0 of 395 3DBenchy
|
||||
Display Progress options:
|
||||
- Display Total Layer Count
|
||||
- Disply Time Remaining for the print
|
||||
- Time Fudge Factor % - Divide the Actual Print Time by the Cura Estimate. Enter as a percentage and the displayed time will be adjusted.
|
||||
This allows you to bring the displayed time closer to reality (Ex: Entering 87.5 would indicate an adjustment to 87.5% of the Cura estimate).
|
||||
- Example line on LCD: 1/479 | ET 2h13m
|
||||
- Time to Pauses changes the M117/M118 lines to countdown to the next pause as 1/479 | TP 2h36m
|
||||
- 'Add M118 Line' is available with either option. M118 will bounce the message back to a remote print server through the USB connection.
|
||||
- 'Add M73 Line' is used by 'Display Progress' only. There are options to incluse M73 P(percent) and M73 R(time remaining)
|
||||
- Enable 'Finish-Time' Message - when enabled, takes the Print Time and calculates when the print will end. It uses the Time Fudge Factor. The user may enter a print start time.
|
||||
Date: June 30, 2025 Cost of electricity added to the other print statistics in '_add_stats'.
|
||||
"""
|
||||
|
||||
from ..Script import Script
|
||||
from UM.Application import Application
|
||||
|
|
@ -37,6 +42,19 @@ from UM.Message import Message
|
|||
|
||||
class DisplayInfoOnLCD(Script):
|
||||
|
||||
def initialize(self) -> None:
|
||||
super().initialize()
|
||||
try:
|
||||
if Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value") == "all_at_once":
|
||||
enable_countdown = True
|
||||
self._instance.setProperty("enable_countdown", "value", enable_countdown)
|
||||
except AttributeError:
|
||||
# Handle cases where the global container stack or its properties are not accessible
|
||||
pass
|
||||
except KeyError:
|
||||
# Handle cases where the "print_sequence" property is missing
|
||||
pass
|
||||
|
||||
def getSettingDataString(self):
|
||||
return """{
|
||||
"name": "Display Info on LCD",
|
||||
|
|
@ -77,7 +95,7 @@ class DisplayInfoOnLCD(Script):
|
|||
"label": "Initial layer number:",
|
||||
"description": "Choose which number you prefer for the initial layer, 0 or 1",
|
||||
"type": "int",
|
||||
"default_value": 0,
|
||||
"default_value": 1,
|
||||
"minimum_value": 0,
|
||||
"maximum_value": 1,
|
||||
"enabled": "display_option == 'filename_layer'"
|
||||
|
|
@ -114,17 +132,40 @@ class DisplayInfoOnLCD(Script):
|
|||
"default_value": true,
|
||||
"enabled": "display_option == 'display_progress'"
|
||||
},
|
||||
"add_m117_line":
|
||||
{
|
||||
"label": "Add M117 Line",
|
||||
"description": "M117 sends a message to the LCD screen. Some screen firmware will not accept or display messages.",
|
||||
"type": "bool",
|
||||
"default_value": true
|
||||
},
|
||||
"add_m118_line":
|
||||
{
|
||||
"label": "Add M118 Line",
|
||||
"description": "Adds M118 in addition to the M117. It will bounce the message back through the USB port to a computer print server (if a printer server like Octoprint or Pronterface is in use).",
|
||||
"type": "bool",
|
||||
"default_value": false
|
||||
"default_value": true
|
||||
},
|
||||
"add_m118_a1":
|
||||
{
|
||||
"label": " Add A1 to M118 Line",
|
||||
"description": "Adds A1 parameter. A1 adds a double foreslash '//' to the response. Octoprint may require this.",
|
||||
"type": "bool",
|
||||
"default_value": false,
|
||||
"enabled": "add_m118_line"
|
||||
},
|
||||
"add_m118_p0":
|
||||
{
|
||||
"label": " Add P0 to M118 Line",
|
||||
"description": "Adds P0 parameter. P0 has the printer send the response out through all it's ports. Octoprint may require this.",
|
||||
"type": "bool",
|
||||
"default_value": false,
|
||||
"enabled": "add_m118_line"
|
||||
},
|
||||
"add_m73_line":
|
||||
{
|
||||
"label": "Add M73 Line(s)",
|
||||
"description": "Adds M73 in addition to the M117. For some firmware this will set the printers time and or percentage.",
|
||||
"description": "Adds M73 in addition to the M117. For some firmware this will set the printers time and or percentage. M75 is added to the beginning of the file and M77 is added to the end of the file. M73 will be added if one or both of the following options is chosen.",
|
||||
"type": "bool",
|
||||
"default_value": false,
|
||||
"enabled": "display_option == 'display_progress'"
|
||||
|
|
@ -132,7 +173,7 @@ class DisplayInfoOnLCD(Script):
|
|||
"add_m73_percent":
|
||||
{
|
||||
"label": " Add M73 Percentage",
|
||||
"description": "Adds M73 with the P parameter. For some firmware this will set the printers 'percentage' of layers completed and it will count upward.",
|
||||
"description": "Adds M73 with the P parameter to the start of each layer. For some firmware this will set the printers 'percentage' of layers completed and it will count upward.",
|
||||
"type": "bool",
|
||||
"default_value": false,
|
||||
"enabled": "add_m73_line and display_option == 'display_progress'"
|
||||
|
|
@ -140,10 +181,10 @@ class DisplayInfoOnLCD(Script):
|
|||
"add_m73_time":
|
||||
{
|
||||
"label": " Add M73 Time",
|
||||
"description": "Adds M73 with the R parameter. For some firmware this will set the printers 'print time' and it will count downward.",
|
||||
"description": "Adds M73 with the R parameter to the start of each layer. For some firmware this will set the printers 'print time' and it will count downward.",
|
||||
"type": "bool",
|
||||
"default_value": false,
|
||||
"enabled": "add_m73_line and display_option == 'display_progress'"
|
||||
"enabled": "add_m73_line and display_option == 'display_progress' and display_remaining_time"
|
||||
},
|
||||
"speed_factor":
|
||||
{
|
||||
|
|
@ -154,13 +195,29 @@ class DisplayInfoOnLCD(Script):
|
|||
"default_value": 100,
|
||||
"enabled": "enable_end_message or display_option == 'display_progress'"
|
||||
},
|
||||
"enable_countdown":
|
||||
{
|
||||
"label": "Enable Countdown to Pauses",
|
||||
"description": "If print sequence is 'one_at_a_time' this is false. This setting is always hidden.",
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"enabled": false
|
||||
},
|
||||
"countdown_to_pause":
|
||||
{
|
||||
"label": "Countdown to Pauses",
|
||||
"description": "Instead of the remaining print time the LCD will show the estimated time to pause (TP).",
|
||||
"description": "This must run AFTER any script that adds a pause. Instead of the remaining print time the LCD will show the estimated time to the next layer that has a pause (TP). Countdown to Pause is not available when in One-at-a-Time' mode.",
|
||||
"type": "bool",
|
||||
"default_value": false,
|
||||
"enabled": "display_option == 'display_progress'"
|
||||
"enabled": "display_option == 'display_progress' and enable_countdown and display_remaining_time"
|
||||
},
|
||||
"pause_cmd":
|
||||
{
|
||||
"label": " What pause command(s) are used?",
|
||||
"description": "This might be M0, or M25 or M600 if Filament Change is used. If you have mixed commands then delimit them with a comma ',' (Ex: M0,M600). Spaces are not allowed.",
|
||||
"type": "str",
|
||||
"default_value": "M0",
|
||||
"enabled": "display_option == 'display_progress' and countdown_to_pause and enable_countdown and display_remaining_time"
|
||||
},
|
||||
"enable_end_message":
|
||||
{
|
||||
|
|
@ -173,11 +230,29 @@ class DisplayInfoOnLCD(Script):
|
|||
"print_start_time":
|
||||
{
|
||||
"label": "Print Start Time (Ex 16:45)",
|
||||
"description": "Use 'Military' time. 16:45 would be 4:45PM. 09:30 would be 9:30AM. If you leave this blank it will be assumed that the print will start Now. If you enter a guesstimate of your printer start time and that time is before 'Now' the guesstimate will consider that the print will start tomorrow at the entered time. ",
|
||||
"description": "Use 'Military' time. 16:45 would be 4:45PM. 09:30 would be 9:30AM. If you leave this blank it will be assumed that the print will start Now. If you enter a guesstimate of your printer start time and that time is before 'Now' then the guesstimate will consider that the print will start tomorrow at the entered time. ",
|
||||
"type": "str",
|
||||
"default_value": "",
|
||||
"unit": "hrs ",
|
||||
"enabled": "enable_end_message"
|
||||
},
|
||||
"electricity_cost":
|
||||
{
|
||||
"label": "Electricity Cost per kWh",
|
||||
"description": "Cost of electricity per kilowatt-hour. This should be on your electric utility bill.",
|
||||
"type": "float",
|
||||
"default_value": 0.151,
|
||||
"minimum_value": 0,
|
||||
"unit": "€/kWh "
|
||||
},
|
||||
"printer_power_usage":
|
||||
{
|
||||
"label": "Printer Power Usage",
|
||||
"description": "Average power usage of the 3D printer in Watts. The actual wattage has many variables. 50% of the power supply rating would be a ballpark figure.",
|
||||
"type": "float",
|
||||
"default_value": 175,
|
||||
"minimum_value": 0,
|
||||
"unit": "Watts "
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -185,239 +260,303 @@ class DisplayInfoOnLCD(Script):
|
|||
|
||||
def execute(self, data):
|
||||
display_option = self.getSettingValueByKey("display_option")
|
||||
add_m118_line = self.getSettingValueByKey("add_m118_line")
|
||||
add_m73_line = self.getSettingValueByKey("add_m73_line")
|
||||
add_m73_time = self.getSettingValueByKey("add_m73_time")
|
||||
add_m73_percent = self.getSettingValueByKey("add_m73_percent")
|
||||
|
||||
# This is Display Filename and Layer on LCD---------------------------------------------------------
|
||||
self.add_m117_line = self.getSettingValueByKey("add_m117_line")
|
||||
self.add_m118_line = self.getSettingValueByKey("add_m118_line")
|
||||
self.add_m118_a1 = self.getSettingValueByKey("add_m118_a1")
|
||||
self.add_m118_p0 = self.getSettingValueByKey("add_m118_p0")
|
||||
self.m118_text = "M118 "
|
||||
self.add_m73_line = self.getSettingValueByKey("add_m73_line")
|
||||
self.add_m73_time = self.getSettingValueByKey("add_m73_time")
|
||||
self.add_m73_percent = self.getSettingValueByKey("add_m73_percent")
|
||||
self.m73_str = ""
|
||||
para_1 = data[0].split("\n")
|
||||
for line in para_1:
|
||||
if line.startswith(";TIME:") or line.startswith(";PRINT.TIME:"):
|
||||
self.time_total = int(line.split(":")[1])
|
||||
break
|
||||
if display_option == "filename_layer":
|
||||
max_layer = 0
|
||||
lcd_text = "M117 "
|
||||
if self.getSettingValueByKey("file_name") != "":
|
||||
file_name = self.getSettingValueByKey("file_name")
|
||||
else:
|
||||
file_name = Application.getInstance().getPrintInformation().jobName
|
||||
if self.getSettingValueByKey("addPrefixPrinting"):
|
||||
lcd_text += "Printing "
|
||||
if not self.getSettingValueByKey("scroll"):
|
||||
lcd_text += "Layer "
|
||||
else:
|
||||
lcd_text += file_name + " - Layer "
|
||||
i = self.getSettingValueByKey("startNum")
|
||||
for layer in data:
|
||||
display_text = lcd_text + str(i)
|
||||
layer_index = data.index(layer)
|
||||
lines = layer.split("\n")
|
||||
for line in lines:
|
||||
if line.startswith(";LAYER_COUNT:"):
|
||||
max_layer = line
|
||||
max_layer = max_layer.split(":")[1]
|
||||
if self.getSettingValueByKey("startNum") == 0:
|
||||
max_layer = str(int(max_layer) - 1)
|
||||
if line.startswith(";LAYER:"):
|
||||
if self.getSettingValueByKey("maxlayer"):
|
||||
display_text = display_text + " of " + max_layer
|
||||
if not self.getSettingValueByKey("scroll"):
|
||||
display_text = display_text + " " + file_name
|
||||
else:
|
||||
if not self.getSettingValueByKey("scroll"):
|
||||
display_text = display_text + " " + file_name + "!"
|
||||
else:
|
||||
display_text = display_text + "!"
|
||||
line_index = lines.index(line)
|
||||
lines.insert(line_index + 1, display_text)
|
||||
if add_m118_line:
|
||||
lines.insert(line_index + 2, str(display_text.replace("M117", "M118", 1)))
|
||||
i += 1
|
||||
final_lines = "\n".join(lines)
|
||||
data[layer_index] = final_lines
|
||||
if bool(self.getSettingValueByKey("enable_end_message")):
|
||||
message_str = self.message_to_user(self.getSettingValueByKey("speed_factor") / 100)
|
||||
Message(title = "Display Info on LCD - Estimated Finish Time", text = message_str[0] + "\n\n" + message_str[1] + "\n" + message_str[2] + "\n" + message_str[3]).show()
|
||||
return data
|
||||
data = self._display_filename_layer(data)
|
||||
else:
|
||||
data = self._display_progress(data)
|
||||
return data
|
||||
|
||||
# Display Progress (from 'Show Progress' and 'Display Progress on LCD')---------------------------------------
|
||||
elif display_option == "display_progress":
|
||||
# get settings
|
||||
display_total_layers = self.getSettingValueByKey("display_total_layers")
|
||||
display_remaining_time = self.getSettingValueByKey("display_remaining_time")
|
||||
speed_factor = self.getSettingValueByKey("speed_factor") / 100
|
||||
m73_time = False
|
||||
m73_percent = False
|
||||
if add_m73_line and add_m73_time:
|
||||
m73_time = True
|
||||
if add_m73_line and add_m73_percent:
|
||||
m73_percent = True
|
||||
# initialize global variables
|
||||
first_layer_index = 0
|
||||
time_total = 0
|
||||
number_of_layers = 0
|
||||
time_elapsed = 0
|
||||
# if at least one of the settings is disabled, there is enough room on the display to display "layer"
|
||||
first_section = data[0]
|
||||
lines = first_section.split("\n")
|
||||
# This is from the original 'Display Filename and Layer on LCD'
|
||||
def _display_filename_layer(self, data: str) -> str:
|
||||
data[0] = self._add_stats(data)
|
||||
max_layer = 0
|
||||
format_option = self.getSettingValueByKey("format_option")
|
||||
lcd_text = "M117 "
|
||||
octo_text = "M118 "
|
||||
if self.getSettingValueByKey("file_name") != "":
|
||||
file_name = self.getSettingValueByKey("file_name")
|
||||
else:
|
||||
file_name = Application.getInstance().getPrintInformation().jobName
|
||||
if self.getSettingValueByKey("addPrefixPrinting"):
|
||||
lcd_text += "Printing "
|
||||
octo_text += "Printing "
|
||||
if not format_option:
|
||||
lcd_text += "Lay "
|
||||
octo_text += "Layer "
|
||||
else:
|
||||
lcd_text += file_name + " - Layer "
|
||||
octo_text += file_name + " - Layer "
|
||||
i = self.getSettingValueByKey("startNum")
|
||||
for layer in data:
|
||||
display_text = lcd_text + str(i)
|
||||
self.m118_text = octo_text + str(i)
|
||||
layer_index = data.index(layer)
|
||||
lines = layer.split("\n")
|
||||
for line in lines:
|
||||
if line.startswith(";TIME:"):
|
||||
tindex = lines.index(line)
|
||||
cura_time = int(line.split(":")[1])
|
||||
print_time = cura_time * speed_factor
|
||||
hhh = print_time/3600
|
||||
hr = round(hhh // 1)
|
||||
mmm = round((hhh % 1) * 60)
|
||||
orig_hhh = cura_time/3600
|
||||
orig_hr = round(orig_hhh // 1)
|
||||
orig_mmm = math.floor((orig_hhh % 1) * 60)
|
||||
orig_sec = round((((orig_hhh % 1) * 60) % 1) * 60)
|
||||
if add_m118_line: lines.insert(tindex + 3,"M118 Adjusted Print Time " + str(hr) + "hr " + str(mmm) + "min")
|
||||
lines.insert(tindex + 3,"M117 ET " + str(hr) + "hr " + str(mmm) + "min")
|
||||
# add M73 line at beginning
|
||||
mins = int(60 * hr + mmm)
|
||||
if m73_time:
|
||||
lines.insert(tindex + 3, "M73 R{}".format(mins))
|
||||
if m73_percent:
|
||||
lines.insert(tindex + 3, "M73 P0")
|
||||
# If Countdonw to pause is enabled then count the pauses
|
||||
pause_str = ""
|
||||
if bool(self.getSettingValueByKey("countdown_to_pause")):
|
||||
pause_count = 0
|
||||
for num in range(2,len(data) - 1, 1):
|
||||
if "PauseAtHeight.py" in data[num]:
|
||||
pause_count += 1
|
||||
pause_str = f" with {pause_count} pause(s)"
|
||||
# This line goes in to convert seconds to hours and minutes
|
||||
lines.insert(tindex + 3, f";Cura Time Estimate: {cura_time}sec = {orig_hr}hr {orig_mmm}min {orig_sec}sec {pause_str}")
|
||||
data[0] = "\n".join(lines)
|
||||
data[len(data)-1] += "M117 Orig Cura Est " + str(orig_hr) + "hr " + str(orig_mmm) + "min\n"
|
||||
if add_m118_line: data[len(data)-1] += "M118 Est w/FudgeFactor " + str(speed_factor * 100) + "% was " + str(hr) + "hr " + str(mmm) + "min\n"
|
||||
if not display_total_layers or not display_remaining_time:
|
||||
base_display_text = "layer "
|
||||
else:
|
||||
base_display_text = ""
|
||||
layer = data[len(data)-1]
|
||||
data[len(data)-1] = layer.replace(";End of Gcode" + "\n", "")
|
||||
data[len(data)-1] += ";End of Gcode" + "\n"
|
||||
# Search for the number of layers and the total time from the start code
|
||||
for index in range(len(data)):
|
||||
data_section = data[index]
|
||||
# We have everything we need, save the index of the first layer and exit the loop
|
||||
if ";LAYER:" in data_section:
|
||||
first_layer_index = index
|
||||
break
|
||||
else:
|
||||
for line in data_section.split("\n"):
|
||||
if line.startswith(";LAYER_COUNT:"):
|
||||
number_of_layers = int(line.split(":")[1])
|
||||
elif line.startswith(";TIME:"):
|
||||
time_total = int(line.split(":")[1])
|
||||
# for all layers...
|
||||
current_layer = 0
|
||||
for layer_counter in range(len(data)-2):
|
||||
current_layer += 1
|
||||
layer_index = first_layer_index + layer_counter
|
||||
display_text = base_display_text
|
||||
display_text += str(current_layer)
|
||||
# create a list where each element is a single line of code within the layer
|
||||
lines = data[layer_index].split("\n")
|
||||
if not ";LAYER:" in data[layer_index]:
|
||||
current_layer -= 1
|
||||
continue
|
||||
# add the total number of layers if this option is checked
|
||||
if display_total_layers:
|
||||
display_text += "/" + str(number_of_layers)
|
||||
# if display_remaining_time is checked, it is calculated in this loop
|
||||
if display_remaining_time:
|
||||
time_remaining_display = " | ET " # initialize the time display
|
||||
m = (time_total - time_elapsed) // 60 # estimated time in minutes
|
||||
m *= speed_factor # correct for printing time
|
||||
m = int(m)
|
||||
h, m = divmod(m, 60) # convert to hours and minutes
|
||||
# add the time remaining to the display_text
|
||||
if h > 0: # if it's more than 1 hour left, display format = xhxxm
|
||||
time_remaining_display += str(h) + "h"
|
||||
if m < 10: # add trailing zero if necessary
|
||||
time_remaining_display += "0"
|
||||
time_remaining_display += str(m) + "m"
|
||||
if line.startswith(";LAYER_COUNT:"):
|
||||
max_layer = line
|
||||
max_layer = max_layer.split(":")[1]
|
||||
if self.getSettingValueByKey("startNum") == 0:
|
||||
max_layer = str(int(max_layer) - 1)
|
||||
if line.startswith(";LAYER:"):
|
||||
if self.getSettingValueByKey("maxlayer"):
|
||||
display_text += "/" + max_layer
|
||||
self.m118_text += "/" + max_layer
|
||||
if not format_option:
|
||||
display_text += "|" + file_name
|
||||
self.m118_text += " | " + file_name
|
||||
else:
|
||||
time_remaining_display += str(m) + "m"
|
||||
display_text += time_remaining_display
|
||||
# find time_elapsed at the end of the layer (used to calculate the remaining time of the next layer)
|
||||
if not current_layer == number_of_layers:
|
||||
for line_index in range(len(lines) - 1, -1, -1):
|
||||
line = lines[line_index]
|
||||
if line.startswith(";TIME_ELAPSED:"):
|
||||
# update time_elapsed for the NEXT layer and exit the loop
|
||||
time_elapsed = int(float(line.split(":")[1]))
|
||||
break
|
||||
# insert the text AFTER the first line of the layer (in case other scripts use ";LAYER:")
|
||||
for l_index, line in enumerate(lines):
|
||||
if line.startswith(";LAYER:"):
|
||||
if not format_option:
|
||||
display_text += "|" + file_name + "!"
|
||||
self.m118_text += " | " + file_name + "!"
|
||||
else:
|
||||
display_text += "!"
|
||||
self.m118_text += "!"
|
||||
line_index = lines.index(line)
|
||||
if self.add_m117_line:
|
||||
lines.insert(line_index + 1, display_text)
|
||||
if self.add_m118_line:
|
||||
if self.add_m118_a1:
|
||||
self.m118_text = self.m118_text.replace("M118 ","M118 A1 ")
|
||||
if self.add_m118_p0:
|
||||
self.m118_text = self.m118_text.replace("M118 ","M118 P0 ")
|
||||
lines.insert(line_index + 2, self.m118_text)
|
||||
i += 1
|
||||
final_lines = "\n".join(lines)
|
||||
data[layer_index] = final_lines
|
||||
if bool(self.getSettingValueByKey("enable_end_message")):
|
||||
message_str = self._message_to_user(self.getSettingValueByKey("speed_factor") / 100)
|
||||
Message(title = "Display Info on LCD - Estimated Finish Time", text = message_str[0] + "\n\n" + message_str[1] + "\n" + message_str[2] + "\n" + message_str[3]).show()
|
||||
return data
|
||||
|
||||
# This is from 'Show Progress on LCD'
|
||||
def _display_progress(self, data: str) -> str:
|
||||
# Add some common print settings to the start of the gcode
|
||||
data[0] = self._add_stats(data)
|
||||
# Get settings
|
||||
print_sequence = Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value")
|
||||
display_total_layers = self.getSettingValueByKey("display_total_layers")
|
||||
display_remaining_time = self.getSettingValueByKey("display_remaining_time")
|
||||
speed_factor = self.getSettingValueByKey("speed_factor") / 100
|
||||
m73_time = False
|
||||
m73_percent = False
|
||||
if self.add_m73_line and self.add_m73_time:
|
||||
m73_time = True
|
||||
if self.add_m73_line and self.add_m73_percent:
|
||||
m73_percent = True
|
||||
if self.add_m73_line:
|
||||
data[1] = "M75\n" + data[1]
|
||||
data[len(data)-1] += "M77\n"
|
||||
# Initialize some variables
|
||||
first_layer_index = 0
|
||||
number_of_layers = 0
|
||||
time_elapsed = 0
|
||||
|
||||
# If at least one of the settings is disabled, there is enough room on the display to display "layer"
|
||||
first_section = data[0]
|
||||
lines = first_section.split("\n")
|
||||
pause_cmd = []
|
||||
for line in lines:
|
||||
if line.startswith(";TIME:"):
|
||||
tindex = lines.index(line)
|
||||
cura_time = int(line.split(":")[1])
|
||||
print_time = cura_time * speed_factor
|
||||
hhh = print_time/3600
|
||||
hr = round(hhh // 1)
|
||||
mmm = round((hhh % 1) * 60)
|
||||
orig_hhh = cura_time/3600
|
||||
orig_hr = round(orig_hhh // 1)
|
||||
orig_mmm = math.floor((orig_hhh % 1) * 60)
|
||||
if self.add_m118_line:
|
||||
lines.insert(len(lines) - 2, f"M118 Adjusted Print Time is {hr} hr {mmm} min")
|
||||
if self.add_m117_line:
|
||||
lines.insert(len(lines) - 2, f"M117 ET {hr} hr {mmm} min")
|
||||
# Add M73 line at beginning
|
||||
mins = int(60 * hr + mmm)
|
||||
if self.add_m73_line and (self.add_m73_time or self.add_m73_percent):
|
||||
if m73_time:
|
||||
self.m73_str += " R{}".format(mins)
|
||||
if m73_percent:
|
||||
self.m73_str += " P0"
|
||||
lines.insert(tindex + 4, "M73" + self.m73_str)
|
||||
# If Countdown to pause is enabled then count the pauses
|
||||
pause_str = ""
|
||||
if bool(self.getSettingValueByKey("countdown_to_pause")):
|
||||
pause_count = 0
|
||||
pause_setting = self.getSettingValueByKey("pause_cmd").upper()
|
||||
if pause_setting != "":
|
||||
pause_cmd = []
|
||||
if "," in pause_setting:
|
||||
pause_cmd = pause_setting.split(",")
|
||||
else:
|
||||
pause_cmd.append(pause_setting)
|
||||
for q in range(0, len(pause_cmd)):
|
||||
pause_cmd[q] = "\n" + pause_cmd[q]
|
||||
for num in range(2,len(data) - 2, 1):
|
||||
for q in range(0,len(pause_cmd)):
|
||||
if pause_cmd[q] in data[num]:
|
||||
pause_count += data[num].count(pause_cmd[q], 0, len(data[num]))
|
||||
pause_str = f"with {pause_count} pause" + ("s" if pause_count > 1 else "")
|
||||
else:
|
||||
pause_str = ""
|
||||
# This line goes in to convert seconds to hours and minutes
|
||||
lines.insert(tindex + 1, f";Cura Time Estimate: {orig_hr}hr {orig_mmm}min {pause_str}")
|
||||
data[0] = "\n".join(lines)
|
||||
if self.add_m117_line:
|
||||
data[len(data)-1] += "M117 Orig Cura Est " + str(orig_hr) + "hr " + str(orig_mmm) + "min\n"
|
||||
if self.add_m118_line:
|
||||
data[len(data)-1] += "M118 Est w/FudgeFactor " + str(speed_factor * 100) + "% was " + str(hr) + "hr " + str(mmm) + "min\n"
|
||||
if not display_total_layers or not display_remaining_time:
|
||||
base_display_text = "layer "
|
||||
else:
|
||||
base_display_text = ""
|
||||
layer = data[len(data)-1]
|
||||
data[len(data)-1] = layer.replace(";End of Gcode" + "\n", "")
|
||||
data[len(data)-1] += ";End of Gcode" + "\n"
|
||||
# Search for the number of layers and the total time from the start code
|
||||
for index in range(len(data)):
|
||||
data_section = data[index]
|
||||
# We have everything we need, save the index of the first layer and exit the loop
|
||||
if ";LAYER:" in data_section:
|
||||
first_layer_index = index
|
||||
break
|
||||
else:
|
||||
for line in data_section.split("\n"):
|
||||
if line.startswith(";LAYER_COUNT:"):
|
||||
number_of_layers = int(line.split(":")[1])
|
||||
if print_sequence == "one_at_a_time":
|
||||
number_of_layers = 1
|
||||
for lay in range(2,len(data)-1,1):
|
||||
if ";LAYER:" in data[lay]:
|
||||
number_of_layers += 1
|
||||
# for all layers...
|
||||
current_layer = 0
|
||||
for layer_counter in range(len(data)-2):
|
||||
current_layer += 1
|
||||
layer_index = first_layer_index + layer_counter
|
||||
display_text = base_display_text
|
||||
display_text += str(current_layer)
|
||||
# create a list where each element is a single line of code within the layer
|
||||
lines = data[layer_index].split("\n")
|
||||
if not ";LAYER:" in data[layer_index]:
|
||||
current_layer -= 1
|
||||
continue
|
||||
# add the total number of layers if this option is checked
|
||||
if display_total_layers:
|
||||
display_text += "/" + str(number_of_layers)
|
||||
# if display_remaining_time is checked, it is calculated in this loop
|
||||
if display_remaining_time:
|
||||
time_remaining_display = " | ET " # initialize the time display
|
||||
m = (self.time_total - time_elapsed) // 60 # estimated time in minutes
|
||||
m *= speed_factor # correct for printing time
|
||||
m = int(m)
|
||||
h, m = divmod(m, 60) # convert to hours and minutes
|
||||
# add the time remaining to the display_text
|
||||
if h > 0: # if it's more than 1 hour left, display format = xhxxm
|
||||
time_remaining_display += str(h) + "h"
|
||||
if m < 10: # add trailing zero if necessary
|
||||
time_remaining_display += "0"
|
||||
time_remaining_display += str(m) + "m"
|
||||
else:
|
||||
time_remaining_display += str(m) + "m"
|
||||
display_text += time_remaining_display
|
||||
# find time_elapsed at the end of the layer (used to calculate the remaining time of the next layer)
|
||||
if not current_layer == number_of_layers:
|
||||
for line_index in range(len(lines) - 1, -1, -1):
|
||||
line = lines[line_index]
|
||||
if line.startswith(";TIME_ELAPSED:"):
|
||||
# update time_elapsed for the NEXT layer and exit the loop
|
||||
time_elapsed = int(float(line.split(":")[1]))
|
||||
break
|
||||
# insert the text AFTER the first line of the layer (in case other scripts use ";LAYER:")
|
||||
for l_index, line in enumerate(lines):
|
||||
if line.startswith(";LAYER:"):
|
||||
if self.add_m117_line:
|
||||
lines[l_index] += "\nM117 " + display_text
|
||||
# add M73 line
|
||||
if self.add_m118_line:
|
||||
m118_text = "\nM118 "
|
||||
if self.add_m118_a1:
|
||||
m118_text += "A1 "
|
||||
if self.add_m118_p0:
|
||||
m118_text += "P0 "
|
||||
lines[l_index] += m118_text + display_text
|
||||
# add M73 line
|
||||
if display_remaining_time:
|
||||
mins = int(60 * h + m)
|
||||
if m73_time:
|
||||
lines[l_index] += "\nM73 R{}".format(mins)
|
||||
if self.add_m73_line and (self.add_m73_time or self.add_m73_percent):
|
||||
self.m73_str = ""
|
||||
if m73_time and display_remaining_time:
|
||||
self.m73_str += " R{}".format(mins)
|
||||
if m73_percent:
|
||||
lines[l_index] += "\nM73 P" + str(round(int(current_layer) / int(number_of_layers) * 100))
|
||||
if add_m118_line:
|
||||
lines[l_index] += "\nM118 " + display_text
|
||||
break
|
||||
# overwrite the layer with the modified layer
|
||||
data[layer_index] = "\n".join(lines)
|
||||
self.m73_str += " P" + str(round(int(current_layer) / int(number_of_layers) * 100))
|
||||
lines[l_index] += "\nM73" + self.m73_str
|
||||
break
|
||||
# overwrite the layer with the modified layer
|
||||
data[layer_index] = "\n".join(lines)
|
||||
|
||||
# If enabled then change the ET to TP for 'Time To Pause'
|
||||
if bool(self.getSettingValueByKey("countdown_to_pause")):
|
||||
time_list = []
|
||||
time_list.append("0")
|
||||
time_list.append("0")
|
||||
this_time = 0
|
||||
pause_index = 1
|
||||
time_list = []
|
||||
if bool(self.getSettingValueByKey("countdown_to_pause")):
|
||||
time_list.append("0")
|
||||
time_list.append("0")
|
||||
this_time = 0
|
||||
pause_index = 1
|
||||
|
||||
# Get the layer times
|
||||
for num in range(2,len(data) - 1):
|
||||
layer = data[num]
|
||||
lines = layer.split("\n")
|
||||
for line in lines:
|
||||
if line.startswith(";TIME_ELAPSED:"):
|
||||
this_time = (float(line.split(":")[1]))*speed_factor
|
||||
time_list.append(str(this_time))
|
||||
if "PauseAtHeight.py" in layer:
|
||||
# Get the layer times
|
||||
for num in range(2,len(data) - 1):
|
||||
layer = data[num]
|
||||
lines = layer.split("\n")
|
||||
for line in lines:
|
||||
if line.startswith(";TIME_ELAPSED:"):
|
||||
this_time = (float(line.split(":")[1]))*speed_factor
|
||||
time_list.append(str(this_time))
|
||||
for p_cmd in pause_cmd:
|
||||
if p_cmd in layer:
|
||||
for qnum in range(num - 1, pause_index, -1):
|
||||
time_list[qnum] = str(float(this_time) - float(time_list[qnum])) + "P"
|
||||
pause_index = num-1
|
||||
break
|
||||
|
||||
# Make the adjustments to the M117 (and M118) lines that are prior to a pause
|
||||
for num in range (2, len(data) - 1,1):
|
||||
layer = data[num]
|
||||
lines = layer.split("\n")
|
||||
for line in lines:
|
||||
# Make the adjustments to the M117 (and M118) lines that are prior to a pause
|
||||
for num in range (2, len(data) - 1,1):
|
||||
layer = data[num]
|
||||
lines = layer.split("\n")
|
||||
for line in lines:
|
||||
try:
|
||||
if line.startswith("M117") and "|" in line and "P" in time_list[num]:
|
||||
M117_line = line.split("|")[0] + "| TP "
|
||||
alt_time = time_list[num][:-1]
|
||||
hhh = int(float(alt_time) / 3600)
|
||||
if hhh > 0:
|
||||
hhr = str(hhh) + "h"
|
||||
else:
|
||||
hhr = ""
|
||||
mmm = ((float(alt_time) / 3600) - (int(float(alt_time) / 3600))) * 60
|
||||
sss = int((mmm - int(mmm)) * 60)
|
||||
mmm = str(round(mmm)) + "m"
|
||||
time_to_go = str(hhr) + str(mmm)
|
||||
if hhr == "": time_to_go = time_to_go + str(sss) + "s"
|
||||
M117_line = M117_line + time_to_go
|
||||
time_to_go = self._get_time_to_go(time_list[num])
|
||||
M117_line = line.split("|")[0] + "| TP " + time_to_go
|
||||
layer = layer.replace(line, M117_line)
|
||||
if line.startswith("M118") and "|" in line and "P" in time_list[num]:
|
||||
time_to_go = self._get_time_to_go(time_list[num])
|
||||
M118_line = line.split("|")[0] + "| TP " + time_to_go
|
||||
layer = layer.replace(line, M118_line)
|
||||
data[num] = layer
|
||||
setting_data = ""
|
||||
if bool(self.getSettingValueByKey("enable_end_message")):
|
||||
message_str = self.message_to_user(speed_factor)
|
||||
Message(title = "[Display Info on LCD] - Estimated Finish Time", text = message_str[0] + "\n\n" + message_str[1] + "\n" + message_str[2] + "\n" + message_str[3]).show()
|
||||
except:
|
||||
continue
|
||||
data[num] = layer
|
||||
if bool(self.getSettingValueByKey("enable_end_message")):
|
||||
message_str = self._message_to_user(data, speed_factor, pause_cmd)
|
||||
Message(title = "[Display Info on LCD] - Estimated Finish Time", text = message_str[0] + "\n\n" + message_str[1] + "\n" + message_str[2] + "\n" + message_str[3]).show()
|
||||
return data
|
||||
|
||||
def message_to_user(self, speed_factor: float):
|
||||
# Message the user of the projected finish time of the print
|
||||
def _message_to_user(self, data: str, speed_factor: float, pause_cmd: str) -> str:
|
||||
"""
|
||||
Message the user of the projected finish time of the print and when any pauses might occur
|
||||
"""
|
||||
print_time = Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601)
|
||||
print_start_time = self.getSettingValueByKey("print_start_time")
|
||||
# If the user entered a print start time make sure it is in the correct format or ignore it.
|
||||
|
|
@ -476,8 +615,97 @@ class DisplayInfoOnLCD(Script):
|
|||
if print_start_time != "":
|
||||
print_start_str = "Print Start Time................." + str(print_start_time) + "hrs"
|
||||
else:
|
||||
print_start_str = "Print Start Time.................Now."
|
||||
print_start_str = "Print Start Time.................Now"
|
||||
estimate_str = "Cura Time Estimate.........." + str(print_time)
|
||||
adjusted_str = "Adjusted Time Estimate..." + str(time_change)
|
||||
finish_str = week_day + " " + str(mo_str) + " " + str(new_time.strftime("%d")) + ", " + str(new_time.strftime("%Y")) + " at " + str(show_hr) + str(new_time.strftime("%M")) + str(show_ampm)
|
||||
return finish_str, estimate_str, adjusted_str, print_start_str
|
||||
finish_str = f"{week_day} {mo_str} {new_time.strftime('%d')}, {new_time.strftime('%Y')} at {show_hr}{new_time.strftime('%M')}{show_ampm}"
|
||||
|
||||
# If there are pauses and if countdown is enabled, then add the time-to-pause to the message.
|
||||
if bool(self.getSettingValueByKey("countdown_to_pause")):
|
||||
num = 1
|
||||
for layer in data:
|
||||
for p_cmd in pause_cmd:
|
||||
if p_cmd in layer or "Do the actual pause" in layer:
|
||||
adjusted_str += "\n" + self._get_time_to_go(layer.split("TIME_ELAPSED:")[1].split("\n")[0]) + " ET from start to pause #" + str(num)
|
||||
num += 1
|
||||
return finish_str, estimate_str, adjusted_str, print_start_str
|
||||
|
||||
def _get_time_to_go(self, time_str: str):
|
||||
"""
|
||||
Converts a time string in seconds to a human-readable format (e.g., "2h30m").
|
||||
:param time_str: The time string in seconds.
|
||||
:return: A formatted string representing the time.
|
||||
"""
|
||||
alt_time = time_str[:-1]
|
||||
total_seconds = float(alt_time)
|
||||
hours = int(total_seconds // 3600)
|
||||
minutes = int((total_seconds % 3600) // 60)
|
||||
seconds = int(total_seconds % 60)
|
||||
time_to_go = f"{hours}h" if hours > 0 else ""
|
||||
time_to_go += f"{minutes}m"
|
||||
if hours == 0:
|
||||
time_to_go += f"{seconds}s"
|
||||
return time_to_go
|
||||
|
||||
def _add_stats(self, data: str) -> str:
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
"""
|
||||
Make a list of the models in the file.
|
||||
Add some of the filament stats to the first section of the gcode.
|
||||
"""
|
||||
model_list = []
|
||||
for mdex, layer in enumerate(data):
|
||||
layer = data[mdex].split("\n")
|
||||
for line in layer:
|
||||
if line.startswith(";MESH:") and "NONMESH" not in line:
|
||||
model_name = line.split(":")[1]
|
||||
if not model_name in model_list:
|
||||
model_list.append(model_name)
|
||||
# Filament stats
|
||||
extruder_count = global_stack.getProperty("machine_extruder_count", "value")
|
||||
layheight_0 = global_stack.getProperty("layer_height_0", "value")
|
||||
init_layer_hgt_line = ";Initial Layer Height: " + f"{layheight_0:.2f}".format(layheight_0)
|
||||
filament_line_t0 = ";Extruder 1 (T0)\n"
|
||||
filament_amount = Application.getInstance().getPrintInformation().materialLengths
|
||||
filament_line_t0 += f"; Filament used: {filament_amount[0]}m\n"
|
||||
filament_line_t0 += f"; Filament Type: {global_stack.extruderList[0].material.getMetaDataEntry("material", "")}\n"
|
||||
filament_line_t0 += f"; Filament Dia.: {global_stack.extruderList[0].getProperty("material_diameter", "value")}mm\n"
|
||||
filament_line_t0 += f"; Nozzle Size : {global_stack.extruderList[0].getProperty("machine_nozzle_size", "value")}mm\n"
|
||||
filament_line_t0 += f"; Print Temp. : {global_stack.extruderList[0].getProperty("material_print_temperature", "value")}°\n"
|
||||
filament_line_t0 += f"; Bed Temp. : {global_stack.extruderList[0].getProperty("material_bed_temperature", "value")}°"
|
||||
|
||||
# if there is more than one extruder then get the stats for the second one.
|
||||
filament_line_t1 = ""
|
||||
if extruder_count > 1:
|
||||
filament_line_t1 = "\n;Extruder 2 (T1)\n"
|
||||
filament_line_t1 += f"; Filament used: {filament_amount[1]}m\n"
|
||||
filament_line_t1 += f"; Filament Type: {global_stack.extruderList[1].material.getMetaDataEntry("material", "")}\n"
|
||||
filament_line_t1 += f"; Filament Dia.: {global_stack.extruderList[1].getProperty("material_diameter", "value")}mm\n"
|
||||
filament_line_t1 += f"; Nozzle Size : {global_stack.extruderList[1].getProperty("machine_nozzle_size", "value")}mm\n"
|
||||
filament_line_t1 += f"; Print Temp. : {global_stack.extruderList[1].getProperty("material_print_temperature", "value")}°"
|
||||
|
||||
# Calculate the cost of electricity for the print
|
||||
electricity_cost = self.getSettingValueByKey("electricity_cost")
|
||||
printer_power_usage = self.getSettingValueByKey("printer_power_usage")
|
||||
currency_unit = Application.getInstance().getPreferences().getValue("cura/currency")
|
||||
total_cost_electricity = (printer_power_usage / 1000) * (self.time_total / 3600) * electricity_cost
|
||||
|
||||
# Add the stats to the gcode file
|
||||
lines = data[0].split("\n")
|
||||
for index, line in enumerate(lines):
|
||||
if line.startswith(";Layer height:") or line.startswith(";TARGET_MACHINE.NAME:"):
|
||||
lines[index] = ";Layer height: " + f"{global_stack.getProperty("layer_height", "value")}"
|
||||
lines[index] += f"\n{init_layer_hgt_line}"
|
||||
lines[index] += f"\n;Base Quality Name : '{global_stack.quality.getMetaDataEntry("name", "")}'"
|
||||
lines[index] += f"\n;Custom Quality Name: '{global_stack.qualityChanges.getMetaDataEntry("name")}'"
|
||||
if line.startswith(";Filament used"):
|
||||
lines[index] = filament_line_t0 + filament_line_t1 + f"\n;Electric Cost: {currency_unit}{total_cost_electricity:.2f}".format(total_cost_electricity)
|
||||
# The target machine "machine_name" is actually the printer model. This adds the user defined printer name to the "TARGET_MACHINE" line.
|
||||
if line.startswith(";TARGET_MACHINE"):
|
||||
machine_model = str(global_stack.getProperty("machine_name", "value"))
|
||||
machine_name = str(global_stack.getName())
|
||||
lines[index] += f" / {machine_name}"
|
||||
if "MINX" in line or "MIN.X" in line:
|
||||
# Add the Object List
|
||||
lines[index - 1] += f"\n;Model List: {str(model_list)}"
|
||||
return "\n".join(lines)
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
@ -734,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()
|
||||
|
|
@ -778,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))
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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,23 +225,26 @@
|
|||
]
|
||||
},
|
||||
"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 },
|
||||
"extra_infill_lines_to_support_skins": { "value": "'walls_and_lines'" },
|
||||
"flooring_layer_count": { "value": 1 },
|
||||
"gradual_flow_enabled": { "value": false },
|
||||
"flooring_material_flow": { "value": "skin_material_flow * 110/93" },
|
||||
"flooring_monotonic": { "value": false },
|
||||
"gradual_flow_discretisation_step_size": { "value": 1 },
|
||||
"gradual_flow_enabled": { "value": true },
|
||||
"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 +288,7 @@
|
|||
{
|
||||
"maximum_value_warning": "machine_max_jerk_xy / 2",
|
||||
"unit": "m/s\u00b3",
|
||||
"value": "jerk_wall_0"
|
||||
"value": "jerk_print"
|
||||
},
|
||||
"jerk_skirt_brim":
|
||||
{
|
||||
|
|
@ -424,7 +431,7 @@
|
|||
},
|
||||
"material_print_temperature": { "maximum_value_warning": 320 },
|
||||
"material_print_temperature_layer_0": { "maximum_value_warning": 320 },
|
||||
"max_flow_acceleration": { "value": 8.0 },
|
||||
"max_flow_acceleration": { "value": 1.5 },
|
||||
"max_skin_angle_for_expansion": { "value": 45 },
|
||||
"meshfix_maximum_resolution": { "value": 0.4 },
|
||||
"min_infill_area": { "default_value": 10 },
|
||||
|
|
@ -438,11 +445,15 @@
|
|||
"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 },
|
||||
"roofing_monotonic": { "value": false },
|
||||
"roofing_pattern": { "value": "'zigzag'" },
|
||||
"seam_overhang_angle": { "value": 35 },
|
||||
"skin_edge_support_thickness": { "value": 0 },
|
||||
"skin_material_flow": { "value": 95 },
|
||||
"skin_overlap": { "value": 0 },
|
||||
"skin_material_flow": { "value": 93 },
|
||||
"skin_outline_count": { "value": 0 },
|
||||
"skin_overlap": { "value": 20 },
|
||||
"skin_preshrink": { "value": 0 },
|
||||
"skirt_brim_minimal_length": { "value": 1000 },
|
||||
"skirt_brim_speed":
|
||||
|
|
@ -538,12 +549,12 @@
|
|||
"speed_wall":
|
||||
{
|
||||
"maximum_value_warning": 300,
|
||||
"value": "speed_print*2/3"
|
||||
"value": "speed_print*1/2"
|
||||
},
|
||||
"speed_wall_0":
|
||||
{
|
||||
"maximum_value_warning": 300,
|
||||
"value": "speed_wall"
|
||||
"value": "speed_wall*60/75"
|
||||
},
|
||||
"speed_wall_0_flooring":
|
||||
{
|
||||
|
|
@ -571,38 +582,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'" }
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ type = intent
|
|||
variant = AA+ 0.4
|
||||
|
||||
[values]
|
||||
hole_xy_offset = 0.075
|
||||
infill_sparse_density = 20
|
||||
inset_direction = outside-in
|
||||
top_bottom_thickness = =wall_thickness
|
||||
wall_thickness = =line_width * 4
|
||||
xy_offset = 0.075
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
[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
|
||||
gradual_flow_enable = False
|
||||
hole_xy_offset = 0.075
|
||||
inset_direction = outside-in
|
||||
speed_wall = =speed_print
|
||||
speed_wall_x = =speed_print
|
||||
speed_wall_x_roofing = =speed_wall
|
||||
wall_line_width_x = =wall_line_width
|
||||
xy_offset = 0.075
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
[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]
|
||||
hole_xy_offset = 0.075
|
||||
infill_sparse_density = 20
|
||||
inset_direction = outside-in
|
||||
top_bottom_thickness = =wall_thickness
|
||||
wall_thickness = =line_width * 4
|
||||
xy_offset = 0.075
|
||||
|
||||
|
|
@ -12,7 +12,10 @@ type = intent
|
|||
variant = AA+ 0.4
|
||||
|
||||
[values]
|
||||
hole_xy_offset = 0.075
|
||||
infill_sparse_density = 20
|
||||
inset_direction = outside-in
|
||||
top_bottom_thickness = =wall_thickness
|
||||
wall_thickness = =line_width * 4
|
||||
xy_offset = 0.075
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
[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]
|
||||
hole_xy_offset = 0.075
|
||||
infill_sparse_density = 20
|
||||
inset_direction = outside-in
|
||||
top_bottom_thickness = =wall_thickness
|
||||
wall_thickness = =line_width * 4
|
||||
xy_offset = 0.075
|
||||
|
||||
|
|
@ -12,7 +12,10 @@ type = intent
|
|||
variant = AA+ 0.4
|
||||
|
||||
[values]
|
||||
hole_xy_offset = 0.075
|
||||
infill_sparse_density = 20
|
||||
inset_direction = outside-in
|
||||
top_bottom_thickness = =wall_thickness
|
||||
wall_thickness = =line_width * 4
|
||||
xy_offset = 0.075
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
[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
|
||||
gradual_flow_enable = False
|
||||
hole_xy_offset = 0.075
|
||||
inset_direction = outside-in
|
||||
speed_wall = =speed_print
|
||||
speed_wall_x = =speed_print
|
||||
speed_wall_x_roofing = =speed_wall
|
||||
wall_line_width_x = =wall_line_width
|
||||
xy_offset = 0.075
|
||||
|
||||
|
|
@ -12,7 +12,10 @@ type = intent
|
|||
variant = AA+ 0.4
|
||||
|
||||
[values]
|
||||
hole_xy_offset = 0.075
|
||||
infill_sparse_density = 20
|
||||
inset_direction = outside-in
|
||||
top_bottom_thickness = =wall_thickness
|
||||
wall_thickness = =line_width * 4
|
||||
xy_offset = 0.075
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
[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
|
||||
gradual_flow_enable = False
|
||||
hole_xy_offset = 0.075
|
||||
inset_direction = outside-in
|
||||
speed_wall = =speed_print
|
||||
speed_wall_x = =speed_print
|
||||
speed_wall_x_roofing = =speed_wall
|
||||
wall_line_width_x = =wall_line_width
|
||||
xy_offset = 0.075
|
||||
|
||||
|
|
@ -12,7 +12,10 @@ type = intent
|
|||
variant = AA+ 0.4
|
||||
|
||||
[values]
|
||||
hole_xy_offset = 0.075
|
||||
infill_sparse_density = 20
|
||||
inset_direction = outside-in
|
||||
top_bottom_thickness = =wall_thickness
|
||||
wall_thickness = =line_width * 4
|
||||
xy_offset = 0.075
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
[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
|
||||
gradual_flow_enable = False
|
||||
hole_xy_offset = 0.075
|
||||
inset_direction = outside-in
|
||||
speed_wall = =speed_print
|
||||
speed_wall_x = =speed_print
|
||||
speed_wall_x_roofing = =speed_wall
|
||||
wall_line_width_x = =wall_line_width
|
||||
xy_offset = 0.075
|
||||
|
||||
|
|
@ -5,6 +5,7 @@ version = 4
|
|||
|
||||
[metadata]
|
||||
intent_category = engineering
|
||||
is_experimental = True
|
||||
material = generic_cpe_plus
|
||||
quality_type = draft
|
||||
setting_version = 25
|
||||
|
|
@ -12,7 +13,10 @@ type = intent
|
|||
variant = CC+ 0.4
|
||||
|
||||
[values]
|
||||
hole_xy_offset = 0.075
|
||||
infill_sparse_density = 20
|
||||
inset_direction = outside-in
|
||||
top_bottom_thickness = =wall_thickness
|
||||
wall_thickness = =line_width * 4
|
||||
xy_offset = 0.075
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ version = 4
|
|||
|
||||
[metadata]
|
||||
intent_category = engineering
|
||||
is_experimental = True
|
||||
material = generic_nylon-cf-slide
|
||||
quality_type = draft
|
||||
setting_version = 25
|
||||
|
|
@ -12,7 +13,10 @@ type = intent
|
|||
variant = CC+ 0.4
|
||||
|
||||
[values]
|
||||
hole_xy_offset = 0.075
|
||||
infill_sparse_density = 20
|
||||
inset_direction = outside-in
|
||||
top_bottom_thickness = =wall_thickness
|
||||
wall_thickness = =line_width * 4
|
||||
xy_offset = 0.075
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ version = 4
|
|||
|
||||
[metadata]
|
||||
intent_category = engineering
|
||||
is_experimental = True
|
||||
material = generic_pc
|
||||
quality_type = draft
|
||||
setting_version = 25
|
||||
|
|
@ -12,7 +13,10 @@ type = intent
|
|||
variant = CC+ 0.4
|
||||
|
||||
[values]
|
||||
hole_xy_offset = 0.075
|
||||
infill_sparse_density = 20
|
||||
inset_direction = outside-in
|
||||
top_bottom_thickness = =wall_thickness
|
||||
wall_thickness = =line_width * 4
|
||||
xy_offset = 0.075
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ version = 4
|
|||
|
||||
[metadata]
|
||||
intent_category = engineering
|
||||
is_experimental = True
|
||||
material = generic_petcf
|
||||
quality_type = draft
|
||||
setting_version = 25
|
||||
|
|
@ -12,7 +13,10 @@ type = intent
|
|||
variant = CC+ 0.4
|
||||
|
||||
[values]
|
||||
hole_xy_offset = 0.075
|
||||
infill_sparse_density = 20
|
||||
inset_direction = outside-in
|
||||
top_bottom_thickness = =wall_thickness
|
||||
wall_thickness = =line_width * 4
|
||||
xy_offset = 0.075
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
[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]
|
||||
hole_xy_offset = 0.075
|
||||
infill_sparse_density = 20
|
||||
inset_direction = outside-in
|
||||
top_bottom_thickness = =wall_thickness
|
||||
wall_thickness = =line_width * 4
|
||||
xy_offset = 0.075
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
[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]
|
||||
hole_xy_offset = 0.075
|
||||
infill_sparse_density = 20
|
||||
inset_direction = outside-in
|
||||
top_bottom_thickness = =wall_thickness
|
||||
wall_thickness = =line_width * 4
|
||||
xy_offset = 0.075
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
130
resources/qml/Preferences/PreferencesDialog.qml
Normal file
130
resources/qml/Preferences/PreferencesDialog.qml
Normal 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"; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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 : ""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -14,7 +14,14 @@ 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
|
||||
hole_xy_offset = 0.1
|
||||
inset_direction = inside-out
|
||||
retraction_prime_speed = 15
|
||||
speed_roofing = =speed_topbottom * 1/3
|
||||
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
|
||||
xy_offset = 0.025
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -13,6 +13,13 @@ weight = -2
|
|||
|
||||
[values]
|
||||
cool_min_layer_time = 4
|
||||
hole_xy_offset = 0.1
|
||||
inset_direction = inside-out
|
||||
material_print_temperature = =default_material_print_temperature + 5
|
||||
retraction_prime_speed = 15
|
||||
speed_roofing = =speed_topbottom * 1/3
|
||||
speed_wall_x = =speed_wall
|
||||
speed_wall_x_roofing = =speed_wall * 0.8
|
||||
wall_line_width_x = =wall_line_width * 1.25
|
||||
xy_offset = 0.025
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,16 @@ variant = AA+ 0.4
|
|||
weight = -1
|
||||
|
||||
[values]
|
||||
cool_min_temperature = =material_print_temperature - 20
|
||||
hole_xy_offset = 0.1
|
||||
inset_direction = inside-out
|
||||
material_final_print_temperature = =material_print_temperature - 15
|
||||
material_initial_print_temperature = =material_print_temperature - 15
|
||||
retraction_prime_speed = =retraction_speed
|
||||
speed_roofing = =speed_topbottom * 1/3
|
||||
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
|
||||
xy_offset = 0.025
|
||||
|
||||
|
|
|
|||
|
|
@ -12,9 +12,17 @@ variant = AA+ 0.4
|
|||
weight = 0
|
||||
|
||||
[values]
|
||||
cool_min_temperature = =material_print_temperature - 20
|
||||
hole_xy_offset = 0.1
|
||||
inset_direction = inside-out
|
||||
material_final_print_temperature = =material_print_temperature - 15
|
||||
material_initial_print_temperature = =material_print_temperature - 15
|
||||
retraction_prime_speed = =retraction_speed
|
||||
speed_roofing = =speed_topbottom * 1/3
|
||||
speed_wall_x = =speed_wall
|
||||
speed_wall_x_roofing = =speed_wall * 0.8
|
||||
support_structure = tree
|
||||
top_bottom_thickness = =round(6*layer_height,3)
|
||||
wall_line_width_x = =wall_line_width * 1.25
|
||||
xy_offset = 0.025
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,16 @@ variant = AA+ 0.4
|
|||
weight = -2
|
||||
|
||||
[values]
|
||||
cool_min_temperature = =material_print_temperature - 20
|
||||
hole_xy_offset = 0.1
|
||||
inset_direction = inside-out
|
||||
material_final_print_temperature = =material_print_temperature - 15
|
||||
material_initial_print_temperature = =material_print_temperature - 15
|
||||
retraction_prime_speed = =retraction_speed
|
||||
speed_roofing = =speed_topbottom * 1/3
|
||||
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
|
||||
xy_offset = 0.025
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,15 @@ variant = AA+ 0.4
|
|||
weight = -1
|
||||
|
||||
[values]
|
||||
cool_min_temperature = =material_print_temperature - 20
|
||||
hole_xy_offset = 0.1
|
||||
inset_direction = inside-out
|
||||
retraction_prime_speed = =retraction_speed
|
||||
retraction_speed = 25
|
||||
speed_roofing = =speed_topbottom * 1/3
|
||||
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
|
||||
xy_offset = 0.025
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,15 @@ variant = AA+ 0.4
|
|||
weight = 0
|
||||
|
||||
[values]
|
||||
cool_min_temperature = =material_print_temperature - 20
|
||||
hole_xy_offset = 0.1
|
||||
inset_direction = inside-out
|
||||
retraction_prime_speed = =retraction_speed
|
||||
speed_roofing = =speed_topbottom * 1/3
|
||||
speed_wall_x = =speed_wall
|
||||
speed_wall_x_roofing = =speed_wall * 0.8
|
||||
support_structure = tree
|
||||
top_bottom_thickness = =round(6*layer_height,3)
|
||||
wall_line_width_x = =wall_line_width * 1.25
|
||||
xy_offset = 0.025
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,14 @@ variant = AA+ 0.4
|
|||
weight = -2
|
||||
|
||||
[values]
|
||||
cool_min_temperature = =material_print_temperature - 20
|
||||
hole_xy_offset = 0.1
|
||||
inset_direction = inside-out
|
||||
retraction_prime_speed = =retraction_speed
|
||||
speed_roofing = =speed_topbottom * 1/3
|
||||
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
|
||||
xy_offset = 0.025
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
[general]
|
||||
definition = ultimaker_s8
|
||||
name = Fast
|
||||
name = Fast - Experimental
|
||||
version = 4
|
||||
|
||||
[metadata]
|
||||
is_experimental = True
|
||||
material = generic_cpe_plus
|
||||
quality_type = draft
|
||||
setting_version = 25
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
[general]
|
||||
definition = ultimaker_s8
|
||||
name = Fast
|
||||
name = Fast - Experimental
|
||||
version = 4
|
||||
|
||||
[metadata]
|
||||
is_experimental = True
|
||||
material = generic_nylon-cf-slide
|
||||
quality_type = draft
|
||||
setting_version = 25
|
||||
|
|
@ -12,6 +13,41 @@ variant = CC+ 0.4
|
|||
weight = -2
|
||||
|
||||
[values]
|
||||
acceleration_roofing = =acceleration_topbottom/2
|
||||
bridge_enable_more_layers = True
|
||||
bridge_skin_density = 70
|
||||
bridge_skin_material_flow = 100
|
||||
bridge_skin_material_flow_2 = 70
|
||||
bridge_skin_speed = 30
|
||||
bridge_skin_speed_2 = =speed_print*2/3
|
||||
bridge_wall_material_flow = 100
|
||||
bridge_wall_min_length = 2
|
||||
bridge_wall_speed = 30
|
||||
cool_min_layer_time = 6
|
||||
cool_min_layer_time_fan_speed_max = 11
|
||||
retraction_prime_speed = 15
|
||||
cool_min_layer_time_overhang = 11
|
||||
cool_min_temperature = =material_print_temperature-10
|
||||
flooring_monotonic = False
|
||||
infill_material_flow = =material_flow if infill_sparse_density < 95 else 95
|
||||
infill_pattern = ='zigzag' if infill_sparse_density > 50 else 'grid'
|
||||
jerk_roofing = =jerk_print
|
||||
material_flow = 95
|
||||
retraction_hop_enabled = False
|
||||
retraction_prime_speed = 25
|
||||
roofing_material_flow = =skin_material_flow
|
||||
roofing_monotonic = False
|
||||
skin_material_flow = =0.95*material_flow
|
||||
skin_outline_count = 0
|
||||
support_bottom_distance = =support_z_distance
|
||||
support_structure = tree
|
||||
support_tree_tip_diameter = 2.0
|
||||
support_tree_top_rate = 10
|
||||
support_xy_distance = 1.2
|
||||
support_xy_distance_overhang = =1.5*machine_nozzle_size
|
||||
support_z_distance = =min(2*layer_height, 0.4)
|
||||
top_bottom_thickness = =wall_thickness
|
||||
wall_0_inset = =0.05
|
||||
wall_overhang_speed_factors = [100,90,80,70,60,50]
|
||||
wall_x_material_flow = =material_flow
|
||||
xy_offset = 0.075
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
[general]
|
||||
definition = ultimaker_s8
|
||||
name = Fast
|
||||
name = Fast - Experimental
|
||||
version = 4
|
||||
|
||||
[metadata]
|
||||
is_experimental = True
|
||||
material = generic_pc
|
||||
quality_type = draft
|
||||
setting_version = 25
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
[general]
|
||||
definition = ultimaker_s8
|
||||
name = Fast
|
||||
name = Fast - Experimental
|
||||
version = 4
|
||||
|
||||
[metadata]
|
||||
is_experimental = True
|
||||
material = generic_petcf
|
||||
quality_type = draft
|
||||
setting_version = 25
|
||||
|
|
@ -12,5 +13,43 @@ variant = CC+ 0.4
|
|||
weight = -2
|
||||
|
||||
[values]
|
||||
acceleration_roofing = =acceleration_topbottom/2
|
||||
adhesion_type = skirt
|
||||
bridge_enable_more_layers = True
|
||||
bridge_skin_density = 70
|
||||
bridge_skin_material_flow = 100
|
||||
bridge_skin_material_flow_2 = 70
|
||||
bridge_skin_speed = 30
|
||||
bridge_skin_speed_2 = =speed_print*2/3
|
||||
bridge_wall_material_flow = 100
|
||||
bridge_wall_min_length = 2
|
||||
bridge_wall_speed = 30
|
||||
cool_min_layer_time = 6
|
||||
cool_min_layer_time_overhang = 11
|
||||
cool_min_temperature = =material_print_temperature-10
|
||||
flooring_monotonic = False
|
||||
infill_material_flow = =material_flow if infill_sparse_density < 95 else 95
|
||||
infill_pattern = ='zigzag' if infill_sparse_density > 50 else 'grid'
|
||||
jerk_roofing = =jerk_print
|
||||
material_pressure_advance_factor = 0.25
|
||||
retraction_hop_enabled = False
|
||||
retraction_prime_speed = 15
|
||||
roofing_material_flow = =skin_material_flow
|
||||
roofing_monotonic = False
|
||||
skin_material_flow = =0.95*material_flow
|
||||
skin_outline_count = 0
|
||||
skirt_height = 5
|
||||
support_bottom_distance = =support_z_distance
|
||||
support_structure = tree
|
||||
support_tree_tip_diameter = 2.0
|
||||
support_tree_top_rate = 10
|
||||
support_xy_distance = 1.2
|
||||
support_xy_distance_overhang = =1.5*machine_nozzle_size
|
||||
support_z_distance = =min(2*layer_height, 0.4)
|
||||
switch_extruder_retraction_amount = 16
|
||||
top_bottom_thickness = =wall_thickness
|
||||
wall_0_inset = =0.05
|
||||
wall_overhang_speed_factors = [100,90,80,70,60,50]
|
||||
wall_x_material_flow = =material_flow
|
||||
xy_offset = 0.075
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
[general]
|
||||
definition = ultimaker_s8
|
||||
name = Fast
|
||||
version = 4
|
||||
|
||||
[metadata]
|
||||
material = generic_nylon-cf-slide
|
||||
quality_type = draft
|
||||
setting_version = 25
|
||||
type = quality
|
||||
variant = CC+ 0.6
|
||||
weight = -2
|
||||
|
||||
[values]
|
||||
acceleration_roofing = =acceleration_topbottom/2
|
||||
bridge_enable_more_layers = True
|
||||
bridge_skin_density = 70
|
||||
bridge_skin_material_flow = 100
|
||||
bridge_skin_material_flow_2 = 70
|
||||
bridge_skin_speed = 30
|
||||
bridge_skin_speed_2 = =speed_print*2/3
|
||||
bridge_wall_material_flow = 100
|
||||
bridge_wall_min_length = 2
|
||||
bridge_wall_speed = 30
|
||||
cool_min_layer_time = 6
|
||||
cool_min_layer_time_fan_speed_max = 11
|
||||
cool_min_layer_time_overhang = 11
|
||||
cool_min_temperature = =material_print_temperature-10
|
||||
flooring_monotonic = False
|
||||
infill_material_flow = =material_flow if infill_sparse_density < 95 else 95
|
||||
infill_pattern = ='zigzag' if infill_sparse_density > 50 else 'grid'
|
||||
jerk_roofing = =jerk_print
|
||||
material_flow = 95
|
||||
retraction_hop_enabled = False
|
||||
retraction_prime_speed = 25
|
||||
roofing_line_width = 0.5
|
||||
roofing_material_flow = =skin_material_flow
|
||||
roofing_monotonic = False
|
||||
skin_material_flow = =0.95*material_flow
|
||||
skin_outline_count = 0
|
||||
speed_print = 80
|
||||
speed_roofing = 50
|
||||
speed_wall = =speed_print
|
||||
speed_wall_0_roofing = =speed_roofing
|
||||
speed_wall_x_roofing = =speed_roofing
|
||||
support_bottom_distance = =support_z_distance
|
||||
support_line_width = 0.6
|
||||
support_structure = tree
|
||||
support_tree_tip_diameter = 2.0
|
||||
support_tree_top_rate = 10
|
||||
support_xy_distance = 1.2
|
||||
support_xy_distance_overhang = =1.5*machine_nozzle_size
|
||||
support_z_distance = =min(2*layer_height, 0.4)
|
||||
top_bottom_thickness = =wall_thickness
|
||||
wall_0_inset = =0.05
|
||||
wall_line_width_0 = 0.5
|
||||
wall_overhang_speed_factors = [100,90,80,70,60,50]
|
||||
wall_x_material_flow = =material_flow
|
||||
xy_offset = 0.075
|
||||
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
[general]
|
||||
definition = ultimaker_s8
|
||||
name = Fast
|
||||
version = 4
|
||||
|
||||
[metadata]
|
||||
material = generic_petcf
|
||||
quality_type = draft
|
||||
setting_version = 25
|
||||
type = quality
|
||||
variant = CC+ 0.6
|
||||
weight = -2
|
||||
|
||||
[values]
|
||||
acceleration_roofing = =acceleration_topbottom/2
|
||||
adhesion_type = skirt
|
||||
bridge_enable_more_layers = True
|
||||
bridge_skin_density = 70
|
||||
bridge_skin_material_flow = 100
|
||||
bridge_skin_material_flow_2 = 70
|
||||
bridge_skin_speed = 30
|
||||
bridge_skin_speed_2 = =speed_print*2/3
|
||||
bridge_wall_material_flow = 100
|
||||
bridge_wall_min_length = 2
|
||||
bridge_wall_speed = 30
|
||||
cool_min_layer_time = 6
|
||||
cool_min_layer_time_overhang = 11
|
||||
cool_min_temperature = =material_print_temperature-10
|
||||
flooring_monotonic = False
|
||||
infill_material_flow = =material_flow if infill_sparse_density < 95 else 95
|
||||
infill_pattern = ='zigzag' if infill_sparse_density > 50 else 'grid'
|
||||
jerk_roofing = =jerk_print
|
||||
material_pressure_advance_factor = 0.25
|
||||
retraction_hop_enabled = False
|
||||
retraction_prime_speed = 15
|
||||
roofing_line_width = 0.5
|
||||
roofing_material_flow = =skin_material_flow
|
||||
roofing_monotonic = False
|
||||
skin_material_flow = =0.95*material_flow
|
||||
skin_outline_count = 0
|
||||
skirt_height = 5
|
||||
speed_print = 80
|
||||
speed_roofing = 50
|
||||
speed_wall = =speed_print
|
||||
speed_wall_0_roofing = =speed_roofing
|
||||
speed_wall_x_roofing = =speed_roofing
|
||||
support_bottom_distance = =support_z_distance
|
||||
support_interface_enable = False
|
||||
support_line_width = 0.6
|
||||
support_structure = tree
|
||||
support_tree_tip_diameter = 2.0
|
||||
support_tree_top_rate = 10
|
||||
support_xy_distance = 1.2
|
||||
support_xy_distance_overhang = =1.5*machine_nozzle_size
|
||||
support_z_distance = =min(2*layer_height, 0.4)
|
||||
switch_extruder_retraction_amount = 16
|
||||
top_bottom_thickness = =wall_thickness
|
||||
wall_0_inset = =0.05
|
||||
wall_line_width_0 = 0.5
|
||||
wall_overhang_speed_factors = [100,90,80,70,60,50]
|
||||
wall_x_material_flow = =material_flow
|
||||
xy_offset = 0.075
|
||||
|
||||
|
|
@ -1,3 +1,10 @@
|
|||
[5.10.2]
|
||||
|
||||
* UltiMaker S6 and S8 improvements:
|
||||
- Introduced the CC+ 0.6 core to the UltiMaker S6 and S8. This core delivers better results for demanding applications and will be replacing the CC+ 0.4 core.
|
||||
- Added new profiles for PC and CPE+ on UltiMaker S6 and S8
|
||||
- Updated the default support type for the PETG material for UltiMaker S6 and S8
|
||||
|
||||
[5.10.1]
|
||||
|
||||
* New features and improvements:
|
||||
|
|
|
|||
5
resources/themes/cura-light/icons/default/Circle.svg
Normal file
5
resources/themes/cura-light/icons/default/Circle.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M 12,2 C 6.5,2 2,6.5 2,12 2,17.5 6.5,22 12,22 17.5,22 22,17.5 22,12 22,6.5 17.5,2 12,2 Z m 0,18 C 7.6,20 4,16.4 4,12 4,7.6 7.6,4 12,4 c 4.4,0 8,3.6 8,8 0,4.4 -3.6,8 -8,8 z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 313 B |
5
resources/themes/cura-light/icons/default/Eraser.svg
Normal file
5
resources/themes/cura-light/icons/default/Eraser.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="22" height="21" viewBox="0 0 22 21" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.5 19.25H4.24999V20.75H21.5V19.25Z" />
|
||||
<path d="M19.535 6.88249L13.5875 0.942493C13.4482 0.803029 13.2827 0.69239 13.1006 0.616904C12.9185 0.541417 12.7234 0.502563 12.5262 0.502563C12.3291 0.502563 12.1339 0.541417 11.9518 0.616904C11.7697 0.69239 11.6043 0.803029 11.465 0.942493L0.964985 11.4425C0.82552 11.5818 0.714882 11.7472 0.639395 11.9293C0.563909 12.1114 0.525055 12.3066 0.525055 12.5037C0.525055 12.7009 0.563909 12.8961 0.639395 13.0782C0.714882 13.2603 0.82552 13.4257 0.964985 13.565L4.34749 17H11.54L19.535 9.00499C19.6745 8.86568 19.7851 8.70025 19.8606 8.51815C19.9361 8.33606 19.9749 8.14087 19.9749 7.94374C19.9749 7.74662 19.9361 7.55143 19.8606 7.36933C19.7851 7.18724 19.6745 7.0218 19.535 6.88249ZM10.9175 15.5H4.99999L1.99998 12.5L6.73249 7.76749L12.68 13.7075L10.9175 15.5ZM13.7375 12.68L7.79749 6.73249L12.5 1.99999L18.5 7.94749L13.7375 12.68Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1,018 B |
6
resources/themes/cura-light/icons/default/Seam.svg
Normal file
6
resources/themes/cura-light/icons/default/Seam.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
id="path2"
|
||||
d="M 6 3 C 4.3000017 3 3 4.3000017 3 6 L 3 18 C 3 19.699998 4.3000017 21 6 21 L 18 21 C 19.699998 21 21 19.699998 21 18 L 21 6 C 21 4.3000017 19.699998 3 18 3 L 6 3 z M 5 5 L 7.6972656 5 L 7.6972656 19 L 5 19 L 5 5 z M 9.5957031 5 L 19 5 L 19 19 L 9.5957031 19 L 9.5957031 16.517578 L 11.369141 16.517578 L 11.369141 14.617188 L 9.5957031 14.617188 L 9.5957031 12.958984 L 11.802734 12.958984 L 11.802734 11.058594 L 9.5957031 11.058594 L 9.5957031 8.890625 L 11.369141 8.890625 L 11.369141 6.9902344 L 9.5957031 6.9902344 L 9.5957031 5 z " />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 694 B |
5
resources/themes/cura-light/icons/low/CancelBadge.svg
Normal file
5
resources/themes/cura-light/icons/low/CancelBadge.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||
<path
|
||||
d="M 6 1 C 3.2385791 1 1 3.2385791 1 6 C 1 8.7614209 3.2385791 11 6 11 C 8.7614209 11 11 8.7614209 11 6 C 11 3.2385791 8.7614209 1 6 1 z M 3.9179688 3.0332031 L 6 5.1171875 L 8.0820312 3.0332031 L 8.9667969 3.9179688 L 6.8828125 6 L 8.9667969 8.0820312 L 8.0820312 8.9667969 L 6 6.8828125 L 3.9179688 8.9667969 L 3.0332031 8.0820312 L 5.1171875 6 L 3.0332031 3.9179688 L 3.9179688 3.0332031 z " />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 532 B |
5
resources/themes/cura-light/icons/low/CheckBadge.svg
Normal file
5
resources/themes/cura-light/icons/low/CheckBadge.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||
<path
|
||||
d="M 6 1 A 5 5 0 0 0 1 6 A 5 5 0 0 0 6 11 A 5 5 0 0 0 11 6 A 5 5 0 0 0 6 1 z M 8.4921875 3.6542969 L 9.3027344 4.4648438 L 5.4199219 8.3457031 L 2.6972656 5.6230469 L 3.5078125 4.8125 L 5.4199219 6.7246094 L 8.4921875 3.6542969 z " />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 369 B |
|
|
@ -504,7 +504,11 @@
|
|||
"error_badge_background": [255, 255, 255, 255],
|
||||
|
||||
"border_field_light": [180, 180, 180, 255],
|
||||
"border_main_light": [212, 212, 212, 255]
|
||||
"border_main_light": [212, 212, 212, 255],
|
||||
|
||||
"paint_normal_area": "background_3",
|
||||
"paint_preferred_area": "um_green_5",
|
||||
"paint_avoid_area": "um_red_5"
|
||||
},
|
||||
|
||||
"sizes": {
|
||||
|
|
|
|||
17
resources/variants/ultimaker_s6_cc_plus06.inst.cfg
Normal file
17
resources/variants/ultimaker_s6_cc_plus06.inst.cfg
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[general]
|
||||
definition = ultimaker_s6
|
||||
name = CC+ 0.6
|
||||
version = 4
|
||||
|
||||
[metadata]
|
||||
hardware_type = nozzle
|
||||
setting_version = 25
|
||||
type = variant
|
||||
|
||||
[values]
|
||||
machine_nozzle_cool_down_speed = 0.9
|
||||
machine_nozzle_id = CC+ 0.6
|
||||
machine_nozzle_size = 0.6
|
||||
machine_nozzle_tip_outer_diameter = 1.2
|
||||
retraction_prime_speed = =retraction_speed
|
||||
|
||||
17
resources/variants/ultimaker_s8_cc_plus06.inst.cfg
Normal file
17
resources/variants/ultimaker_s8_cc_plus06.inst.cfg
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[general]
|
||||
definition = ultimaker_s8
|
||||
name = CC+ 0.6
|
||||
version = 4
|
||||
|
||||
[metadata]
|
||||
hardware_type = nozzle
|
||||
setting_version = 25
|
||||
type = variant
|
||||
|
||||
[values]
|
||||
machine_nozzle_cool_down_speed = 0.9
|
||||
machine_nozzle_id = CC+ 0.6
|
||||
machine_nozzle_size = 0.6
|
||||
machine_nozzle_tip_outer_diameter = 1.2
|
||||
retraction_prime_speed = =retraction_speed
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue