Merge branch 'main' into PP-574-Generic-BVOH
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:
Erwan MATHIEU 2025-09-10 14:50:32 +02:00 committed by GitHub
commit a592ff3a1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 1896 additions and 534 deletions

View file

@ -59,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
@ -361,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)
@ -1034,7 +1038,6 @@ class CuraApplication(QtApplication):
# Initialize UI state
controller.setActiveStage("PrepareStage")
controller.setActiveView("SolidView")
controller.setCameraTool("CameraTool")
controller.setSelectionTool("SelectionTool")
@ -2085,9 +2088,7 @@ class CuraApplication(QtApplication):
is_non_sliceable = "." + file_extension in self._non_sliceable_extensions
if is_non_sliceable:
# Need to switch first to the preview stage and then to layer view
self.callLater(lambda: (self.getController().setActiveStage("PreviewStage"),
self.getController().setActiveView("SimulationView")))
self.callLater(lambda: (self.getController().setActiveStage("PreviewStage")))
block_slicing_decorator = BlockSlicingDecorator()
node.addDecorator(block_slicing_decorator)

46
cura/CuraRenderer.py Normal file
View file

@ -0,0 +1,46 @@
# Copyright (c) 2025 UltiMaker
# Uranium is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING
from cura.PickingPass import PickingPass
from UM.Qt.QtRenderer import QtRenderer
from UM.View.RenderPass import RenderPass
from UM.View.SelectionPass import SelectionPass
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
class CuraRenderer(QtRenderer):
"""An overridden Renderer implementation that adds some behaviors specific to Cura."""
def __init__(self, application: "CuraApplication") -> None:
super().__init__()
self._controller = application.getController()
self._controller.activeToolChanged.connect(self._onActiveToolChanged)
self._extra_rendering_passes: list[RenderPass] = []
def _onActiveToolChanged(self) -> None:
tool_extra_rendering_passes = []
active_tool = self._controller.getActiveTool()
if active_tool is not None:
tool_extra_rendering_passes = active_tool.getRequiredExtraRenderingPasses()
for extra_rendering_pass in self._extra_rendering_passes:
extra_rendering_pass.setEnabled(extra_rendering_pass.getName() in tool_extra_rendering_passes)
def _makeRenderPasses(self) -> list[RenderPass]:
self._extra_rendering_passes = [
SelectionPass(self._viewport_width, self._viewport_height, SelectionPass.SelectionMode.FACES),
PickingPass(self._viewport_width, self._viewport_height, only_selected_objects=True),
PickingPass(self._viewport_width, self._viewport_height, only_selected_objects=False)
]
for extra_rendering_pass in self._extra_rendering_passes:
extra_rendering_pass.setEnabled(False)
return super()._makeRenderPasses() + self._extra_rendering_passes

View file

@ -7,6 +7,7 @@ from UM.Qt.QtApplication import QtApplication
from UM.Logger import Logger
from UM.Math.Vector import Vector
from UM.Resources import Resources
from UM.Scene.Selection import Selection
from UM.View.RenderPass import RenderPass
from UM.View.GL.OpenGL import OpenGL
@ -27,13 +28,14 @@ class PickingPass(RenderPass):
.. note:: that in order to increase precision, the 24 bit depth value is encoded into all three of the R,G & B channels
"""
def __init__(self, width: int, height: int) -> None:
super().__init__("picking", width, height)
def __init__(self, width: int, height: int, only_selected_objects: bool = False) -> None:
super().__init__("picking" if not only_selected_objects else "picking_selected", width, height)
self._renderer = QtApplication.getInstance().getRenderer()
self._shader = None #type: Optional[ShaderProgram]
self._scene = QtApplication.getInstance().getController().getScene()
self._only_selected_objects = only_selected_objects
def render(self) -> None:
if not self._shader:
@ -53,7 +55,7 @@ class PickingPass(RenderPass):
# Fill up the batch with objects that can be sliced. `
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible() and (not self._only_selected_objects or Selection.isSelected(node)):
batch.addItem(node.getWorldTransformation(copy = False), node.getMeshData(), normal_transformation=node.getCachedNormalMatrix())
self.bind()

View file

@ -1,6 +1,8 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
from PyQt6.QtCore import pyqtProperty, QUrl
from UM.Stage import Stage
@ -13,8 +15,8 @@ from UM.Stage import Stage
# * The MainComponent is the component that will be drawn starting from the bottom of the stageBar and fills the rest
# of the screen.
class CuraStage(Stage):
def __init__(self, parent = None) -> None:
super().__init__(parent)
def __init__(self, parent = None, active_view: Optional[str] = "SolidView") -> None:
super().__init__(parent, active_view = active_view)
@pyqtProperty(str, constant = True)
def stageId(self) -> str:

View file

@ -16,7 +16,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
class XRayPass(RenderPass):
def __init__(self, width, height):
super().__init__("xray", width, height)
super().__init__("xray", width, height, -100)
self._shader = None
self._gl = OpenGL.getInstance().getBindingsObject()

View file

@ -13,13 +13,6 @@ UM.ToolbarButton
property string color
checked: base.selectedColor === buttonBrushColor.color
onClicked: setColor()
function setColor()
{
base.selectedColor = buttonBrushColor.color
UM.Controller.triggerActionWithData("setBrushColor", buttonBrushColor.color)
}
checked: UM.Controller.properties.getValue("BrushColor") === buttonBrushColor.color
onClicked: UM.Controller.setProperty("BrushColor", buttonBrushColor.color)
}

View file

@ -13,13 +13,6 @@ UM.ToolbarButton
property int shape
checked: base.selectedShape === buttonBrushShape.shape
onClicked: setShape()
function setShape()
{
base.selectedShape = buttonBrushShape.shape
UM.Controller.triggerActionWithData("setBrushShape", buttonBrushShape.shape)
}
checked: UM.Controller.properties.getValue("BrushShape") === buttonBrushShape.shape
onClicked: UM.Controller.setProperty("BrushShape", buttonBrushShape.shape)
}

View file

@ -6,19 +6,13 @@ import QtQuick
import UM 1.7 as UM
import Cura 1.0 as Cura
Cura.ModeSelectorButton
{
id: modeSelectorButton
property string mode
selected: base.selectedMode === modeSelectorButton.mode
onClicked: setMode()
function setMode()
{
base.selectedMode = modeSelectorButton.mode
UM.Controller.triggerActionWithData("setPaintType", modeSelectorButton.mode)
}
selected: UM.Controller.properties.getValue("PaintType") === modeSelectorButton.mode
onClicked: UM.Controller.setProperty("PaintType", modeSelectorButton.mode)
}

View file

@ -12,13 +12,18 @@ 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):
@ -30,10 +35,22 @@ class PaintTool(Tool):
SQUARE = 0
CIRCLE = 1
def __init__(self) -> None:
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
@ -41,9 +58,9 @@ class PaintTool(Tool):
self._mesh_transformed_cache = None
self._cache_dirty: bool = True
self._brush_size: int = 10
self._brush_color: str = ""
self._brush_shape: PaintTool.Brush.Shape = PaintTool.Brush.Shape.SQUARE
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
@ -52,6 +69,14 @@ class PaintTool(Tool):
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)
@ -86,61 +111,75 @@ class PaintTool(Tool):
return stroke_image, (start_x, start_y)
def getPaintType(self) -> str:
return self._view.getPaintType()
def setPaintType(self, paint_type: str) -> None:
paint_view = self._get_paint_view()
if paint_view is None:
return
if paint_type != self.getPaintType():
self._view.setPaintType(paint_type)
paint_view.setPaintType(paint_type)
self._brush_pen = self._createBrushPen()
self._updateScene()
self.propertyChanged.emit()
self._brush_pen = self._createBrushPen()
self._updateScene()
def getBrushSize(self) -> int:
return self._brush_size
def setBrushSize(self, brush_size: float) -> None:
if brush_size != self._brush_size:
self._brush_size = int(brush_size)
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:
self._brush_color = brush_color
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 undoStackAction(self, redo_instead: bool) -> bool:
paint_view = self._get_paint_view()
if paint_view is None:
return False
def getCanUndo(self) -> bool:
return self._view.canUndo()
if redo_instead:
paint_view.redoStroke()
else:
paint_view.undoStroke()
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()
return True
def clear(self) -> None:
paintview = self._get_paint_view()
if paintview is None:
return
width, height = paintview.getUvTexDimensions()
width, height = self._view.getUvTexDimensions()
clear_image = QImage(width, height, QImage.Format.Format_RGB32)
clear_image.fill(Qt.GlobalColor.white)
paintview.addStroke(clear_image, 0, 0, "none")
self._view.addStroke(clear_image, 0, 0, "none", False)
self._updateScene()
@staticmethod
def _get_paint_view() -> Optional[PaintView]:
paint_view = Application.getInstance().getController().getActiveView()
if paint_view is None or paint_view.getPluginId() != "PaintTool":
return None
return cast(PaintView, paint_view)
@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
@ -179,7 +218,7 @@ class PaintTool(Tool):
self._cache_dirty = True
def _getTexCoordsFromClick(self, node: SceneNode, x: float, y: float) -> Tuple[int, Optional[numpy.ndarray]]:
face_id = self._selection_pass.getFaceIdAtPosition(x, y)
face_id = self._faces_selection_pass.getFaceIdAtPosition(x, y)
if face_id < 0 or face_id >= node.getMeshData().getFaceCount():
return face_id, None
@ -250,15 +289,14 @@ class PaintTool(Tool):
# Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes
if event.type == Event.ToolActivateEvent:
controller.setActiveStage("PrepareStage")
controller.setActiveView("PaintTool") # Because that's the plugin-name, and the view is registered to it.
return True
if event.type == Event.ToolDeactivateEvent:
controller.setActiveStage("PrepareStage")
controller.setActiveView("SolidView")
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
@ -281,12 +319,15 @@ class PaintTool(Tool):
else:
self._mouse_held = True
paintview = self._get_paint_view()
if paintview is None:
return False
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._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:
@ -297,18 +338,13 @@ class PaintTool(Tool):
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
if not self._picking_pass:
self._picking_pass = PickingPass(camera.getViewportWidth(), camera.getViewportHeight())
self._picking_pass.render()
self._selection_pass.renderFacesMode()
face_id, texcoords = self._getTexCoordsFromClick(node, mouse_evt.x, mouse_evt.y)
if texcoords is None:
return False
@ -325,7 +361,7 @@ class PaintTool(Tool):
(self._last_mouse_coords, (self._last_face_id, self._last_text_coords)),
((mouse_evt.x, mouse_evt.y), (face_id, texcoords)))
w, h = paintview.getUvTexDimensions()
w, h = self._view.getUvTexDimensions()
for start_coords, end_coords in substrokes:
sub_image, (start_x, start_y) = self._createStrokeImage(
start_coords[0] * w,
@ -333,7 +369,7 @@ class PaintTool(Tool):
end_coords[0] * w,
end_coords[1] * h
)
paintview.addStroke(sub_image, start_x, start_y, self._brush_color)
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)
@ -343,9 +379,46 @@ class PaintTool(Tool):
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)
Application.getInstance().getController().getScene().sceneChanged.emit(node)
def _onSelectionChanged(self):
super()._onSelectionChanged()
self.setActiveView("PaintTool" if len(Selection.getAllSelectedObjects()) == 1 else None)
self._updateState()
def _updateState(self):
if len(Selection.getAllSelectedObjects()) == 1 and self._controller.getActiveTool() == self:
selected_object = Selection.getSelectedObject(0)
if selected_object.callDecoration("getPaintTexture") is not None:
new_state = PaintTool.Paint.State.READY
else:
new_state = PaintTool.Paint.State.PREPARING_MODEL
self._prepare_texture_job = PrepareTextureJob(selected_object)
self._prepare_texture_job.finished.connect(self._onPrepareTextureFinished)
self._prepare_texture_job.start()
else:
new_state = PaintTool.Paint.State.MULTIPLE_SELECTION
if new_state != self._state:
self._state = new_state
self.propertyChanged.emit()
def _onPrepareTextureFinished(self, job: Job):
if job == self._prepare_texture_job:
self._prepare_texture_job = None
self._state = PaintTool.Paint.State.READY
self.propertyChanged.emit()
def _updateIgnoreUnselectedObjects(self):
ignore_unselected_objects = self._controller.getActiveView().name == "PaintTool"
CuraApplication.getInstance().getRenderer().getRenderPass("selection").setIgnoreUnselectedObjects(ignore_unselected_objects)
CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces").setIgnoreUnselectedObjects(ignore_unselected_objects)

View file

@ -15,22 +15,20 @@ Item
height: childrenRect.height
UM.I18nCatalog { id: catalog; name: "cura"}
property string selectedMode: ""
property string selectedColor: ""
property int selectedShape: 0
Action
{
id: undoAction
shortcut: "Ctrl+L"
onTriggered: UM.Controller.triggerActionWithData("undoStackAction", false)
enabled: UM.Controller.properties.getValue("CanUndo")
onTriggered: UM.Controller.triggerAction("undoStackAction")
}
Action
{
id: redoAction
shortcut: "Ctrl+Shift+L"
onTriggered: UM.Controller.triggerActionWithData("undoStackAction", true)
enabled: UM.Controller.properties.getValue("CanRedo")
onTriggered: UM.Controller.triggerAction("redoStackAction")
}
Column
@ -57,6 +55,7 @@ Item
icon: "Support"
tooltipText: catalog.i18nc("@tooltip", "Refine support placement by defining preferred/avoidance areas")
mode: "support"
visible: false
}
}
@ -166,20 +165,15 @@ Item
from: 10
to: 1000
value: 200
value: UM.Controller.properties.getValue("BrushSize")
onPressedChanged: function(pressed)
{
if(! pressed)
{
setBrushSize()
UM.Controller.setProperty("BrushSize", shapeSizeSlider.value);
}
}
function setBrushSize()
{
UM.Controller.triggerActionWithData("setBrushSize", shapeSizeSlider.value)
}
}
//Line between the sections.
@ -196,6 +190,7 @@ Item
{
id: undoButton
enabled: undoAction.enabled
text: catalog.i18nc("@action:button", "Undo Stroke")
toolItem: UM.ColorImage
{
@ -210,6 +205,7 @@ Item
{
id: redoButton
enabled: redoAction.enabled
text: catalog.i18nc("@action:button", "Redo Stroke")
toolItem: UM.ColorImage
{
@ -232,12 +228,74 @@ Item
}
}
Component.onCompleted:
Rectangle
{
// Force first types for consistency, otherwise UI may become different from controller
rowPaintMode.children[0].setMode()
rowBrushColor.children[1].setColor()
rowBrushShape.children[1].setShape()
shapeSizeSlider.setBrushSize()
id: waitPrepareItem
anchors.fill: parent
color: UM.Theme.getColor("main_background")
visible: UM.Controller.properties.getValue("State") === Cura.PaintToolState.PREPARING_MODEL
ColumnLayout
{
anchors.fill: parent
UM.Label
{
Layout.fillWidth: true
Layout.fillHeight: true
Layout.verticalStretchFactor: 2
text: catalog.i18nc("@label", "Preparing model for painting...")
verticalAlignment: Text.AlignBottom
horizontalAlignment: Text.AlignHCenter
}
Item
{
Layout.preferredWidth: loadingIndicator.width
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: true
Layout.verticalStretchFactor: 1
UM.ColorImage
{
id: loadingIndicator
anchors.top: parent.top
anchors.left: parent.left
width: UM.Theme.getSize("card_icon").width
height: UM.Theme.getSize("card_icon").height
source: UM.Theme.getIcon("ArrowDoubleCircleRight")
color: UM.Theme.getColor("text_default")
RotationAnimator
{
target: loadingIndicator
from: 0
to: 360
duration: 2000
loops: Animation.Infinite
running: true
alwaysRunToEnd: true
}
}
}
}
}
Rectangle
{
id: selectSingleMessageItem
anchors.fill: parent
color: UM.Theme.getColor("main_background")
visible: UM.Controller.properties.getValue("State") === Cura.PaintToolState.MULTIPLE_SELECTION
UM.Label
{
anchors.fill: parent
text: catalog.i18nc("@label", "Select a single model to start painting")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}
}

View file

@ -0,0 +1,104 @@
# Copyright (c) 2025 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
from typing import cast, Optional
from PyQt6.QtCore import QRect, QPoint
from PyQt6.QtGui import QUndoCommand, QImage, QPainter
from UM.View.GL.Texture import Texture
class PaintUndoCommand(QUndoCommand):
"""Provides the command that does the actual painting on objects with undo/redo mechanisms"""
def __init__(self,
texture: Texture,
stroke_mask: QImage,
x: int,
y: int,
set_value: int,
bit_range: tuple[int, int],
mergeable: bool) -> None:
super().__init__()
self._original_texture_image: Optional[QImage] = texture.getImage().copy() if not mergeable else None
self._texture: Texture = texture
self._stroke_mask: QImage = stroke_mask
self._x: int = x
self._y: int = y
self._set_value: int = set_value
self._bit_range: tuple[int, int] = bit_range
self._mergeable: bool = mergeable
def id(self) -> int:
# Since the undo stack will contain only commands of this type, we can use a fixed ID
return 0
def redo(self) -> None:
actual_image = self._texture.getImage()
bit_range_start, bit_range_end = self._bit_range
full_int32 = 0xffffffff
clear_texture_bit_mask = full_int32 ^ (((full_int32 << (32 - 1 - (bit_range_end - bit_range_start))) & full_int32) >> (
32 - 1 - bit_range_end))
image_rect = QRect(0, 0, self._stroke_mask.width(), self._stroke_mask.height())
clear_bits_image = self._stroke_mask.copy()
clear_bits_image.invertPixels()
painter = QPainter(clear_bits_image)
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Lighten)
painter.fillRect(image_rect, clear_texture_bit_mask)
painter.end()
set_value_image = self._stroke_mask.copy()
painter = QPainter(set_value_image)
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Multiply)
painter.fillRect(image_rect, self._set_value)
painter.end()
stroked_image = actual_image.copy(self._x, self._y, self._stroke_mask.width(), self._stroke_mask.height())
painter = QPainter(stroked_image)
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceAndDestination)
painter.drawImage(0, 0, clear_bits_image)
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceOrDestination)
painter.drawImage(0, 0, set_value_image)
painter.end()
self._texture.setSubImage(stroked_image, self._x, self._y)
def undo(self) -> None:
if self._original_texture_image is not None:
self._texture.setSubImage(self._original_texture_image.copy(self._x,
self._y,
self._stroke_mask.width(),
self._stroke_mask.height()),
self._x,
self._y)
def mergeWith(self, command: QUndoCommand) -> bool:
if not isinstance(command, PaintUndoCommand):
return False
paint_undo_command = cast(PaintUndoCommand, command)
if not paint_undo_command._mergeable:
return False
self_rect = QRect(QPoint(self._x, self._y), self._stroke_mask.size())
command_rect = QRect(QPoint(paint_undo_command._x, paint_undo_command._y), paint_undo_command._stroke_mask.size())
bounding_rect = self_rect.united(command_rect)
merged_mask = QImage(bounding_rect.width(), bounding_rect.height(), self._stroke_mask.format())
merged_mask.fill(0)
painter = QPainter(merged_mask)
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Lighten)
painter.drawImage(self._x - bounding_rect.x(), self._y - bounding_rect.y(), self._stroke_mask)
painter.drawImage(paint_undo_command._x - bounding_rect.x(), paint_undo_command._y - bounding_rect.y(), paint_undo_command._stroke_mask)
painter.end()
self._x = bounding_rect.x()
self._y = bounding_rect.y()
self._stroke_mask = merged_mask
return True

View file

@ -2,49 +2,61 @@
# Cura is released under the terms of the LGPLv3 or higher.
import os
from PyQt6.QtCore import QRect
from typing import Optional, List, Tuple, Dict
from PyQt6.QtCore import QRect, pyqtSignal
from typing import Optional, Dict
from PyQt6.QtGui import QImage, QColor, QPainter
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.View.View import View
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(View):
class PaintView(CuraView):
"""View for model-painting."""
UNDO_STACK_SIZE = 1024
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__()
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._stroke_undo_stack: List[Tuple[QImage, int, int]] = []
self._stroke_redo_stack: List[Tuple[QImage, int, int]] = []
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)
self._force_opaque_mask = QImage(2, 2, QImage.Format.Format_Mono)
self._force_opaque_mask.fill(1)
application = CuraApplication.getInstance()
application.engineCreatedSignal.connect(self._makePaintModes)
self._scene = application.getController().getScene()
CuraApplication.getInstance().engineCreatedSignal.connect(self._makePaintModes)
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()
@ -56,109 +68,73 @@ class PaintView(View):
"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 _forceOpaqueDeepCopy(self, image: QImage):
res = QImage(image.width(), image.height(), QImage.Format.Format_RGBA8888)
res.fill(QColor(255, 255, 255, 255))
painter = QPainter(res)
painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
painter.drawImage(0, 0, image)
painter.end()
res.setAlphaChannel(self._force_opaque_mask.scaled(image.width(), image.height()))
return res
def addStroke(self, stroke_mask: QImage, start_x: int, start_y: int, brush_color: str) -> None:
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
actual_image = self._current_paint_texture.getImage()
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 << self._current_bits_ranges[0]
full_int32 = 0xffffffff
clear_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, stroke_mask.width(), stroke_mask.height())
set_value = self._paint_modes[self._current_paint_type][brush_color].value << bit_range_start
clear_bits_image = stroke_mask.copy()
clear_bits_image.invertPixels()
painter = QPainter(clear_bits_image)
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Lighten)
painter.fillRect(image_rect, clear_mask)
painter.end()
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))
set_value_image = stroke_mask.copy()
painter = QPainter(set_value_image)
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Multiply)
painter.fillRect(image_rect, set_value)
painter.end()
def undoStroke(self) -> None:
self._paint_undo_stack.undo()
stroked_image = actual_image.copy(start_x, start_y, stroke_mask.width(), 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._stroke_redo_stack.clear()
if len(self._stroke_undo_stack) >= PaintView.UNDO_STACK_SIZE:
self._stroke_undo_stack.pop(0)
undo_image = self._forceOpaqueDeepCopy(self._current_paint_texture.setSubImage(stroked_image, start_x, start_y))
if undo_image is not None:
self._stroke_undo_stack.append((undo_image, start_x, start_y))
def _applyUndoStacksAction(self, from_stack: List[Tuple[QImage, int, int]], to_stack: List[Tuple[QImage, int, int]]) -> bool:
if len(from_stack) <= 0 or self._current_paint_texture is None:
return False
from_image, x, y = from_stack.pop()
to_image = self._forceOpaqueDeepCopy(self._current_paint_texture.setSubImage(from_image, x, y))
if to_image is None:
return False
if len(to_stack) >= PaintView.UNDO_STACK_SIZE:
to_stack.pop(0)
to_stack.append((to_image, x, y))
return True
def undoStroke(self) -> bool:
return self._applyUndoStacksAction(self._stroke_undo_stack, self._stroke_redo_stack)
def redoStroke(self) -> bool:
return self._applyUndoStacksAction(self._stroke_redo_stack, self._stroke_undo_stack)
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 paint_type not in paint_data_mapping:
new_mapping = self._add_mapping(paint_data_mapping, len(self._paint_modes[paint_type]))
paint_data_mapping[paint_type] = new_mapping
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)
mesh = node.getMeshData()
if not mesh.hasUVCoordinates():
texture_width, texture_height = mesh.calculateUnwrappedUVCoordinates()
if texture_width > 0 and texture_height > 0:
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)
self._current_paint_type = paint_type
self._current_bits_ranges = paint_data_mapping[paint_type]
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]:
@ -171,17 +147,23 @@ class PaintView(View):
return start_index, end_index
def beginRendering(self) -> None:
renderer = self.getRenderer()
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)
node = Selection.getSelectedObject(0)
if node is None:
return
if self._current_paint_type == "":
return
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])
@ -189,8 +171,3 @@ class PaintView(View):
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)
self._current_paint_texture = node.callDecoration("getPaintTexture")
self._paint_shader.setTexture(0, self._current_paint_texture)
paint_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData(), normal_transformation=node.getCachedNormalMatrix())

View file

@ -0,0 +1,33 @@
# Copyright (c) 2025 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Job import Job
from UM.Scene.SceneNode import SceneNode
from UM.View.GL.OpenGL import OpenGL
class PrepareTextureJob(Job):
"""
Background job to prepare a model for painting, i.e. do the UV-unwrapping and create the appropriate texture image,
which can last a few seconds
"""
def __init__(self, node: SceneNode):
super().__init__()
self._node: SceneNode = node
def run(self) -> None:
# If the model has already-provided UV coordinates, we can only assume that the associated texture
# should be a square
texture_width = texture_height = 4096
mesh = self._node.getMeshData()
if not mesh.hasUVCoordinates():
texture_width, texture_height = mesh.calculateUnwrappedUVCoordinates()
self._node.callDecoration("prepareTexture", texture_width, texture_height)
if hasattr(mesh, OpenGL.VertexBufferProperty):
# Force clear OpenGL buffer so that new UV coordinates will be sent
delattr(mesh, OpenGL.VertexBufferProperty)

View file

@ -27,7 +27,9 @@ def getMetaData():
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": PaintView.PaintView()
"tool": PaintTool.PaintTool(view),
"view": view
}

View file

@ -29,7 +29,6 @@ fragment =
uniform mediump vec4 u_ambientColor;
uniform highp vec3 u_lightPosition;
uniform highp vec3 u_viewPosition;
uniform mediump float u_opacity;
uniform sampler2D u_texture;
uniform mediump int u_bitsRangesStart;
uniform mediump int u_bitsRangesEnd;
@ -55,10 +54,10 @@ fragment =
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 = clamp(dot(normal, light_dir), 0.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 = u_opacity;
final_color.a = 1.0;
frag_color = final_color;
}
@ -95,7 +94,6 @@ fragment41core =
uniform mediump vec4 u_ambientColor;
uniform highp vec3 u_lightPosition;
uniform highp vec3 u_viewPosition;
uniform mediump float u_opacity;
uniform sampler2D u_texture;
uniform mediump int u_bitsRangesStart;
uniform mediump int u_bitsRangesEnd;
@ -122,17 +120,16 @@ fragment41core =
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 = clamp(dot(normal, light_dir), 0.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 = u_opacity;
final_color.a = 1.0;
frag_color = final_color;
}
[defaults]
u_ambientColor = [0.3, 0.3, 0.3, 1.0]
u_opacity = 0.5
u_texture = 0
[bindings]

View 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

View file

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

View file

@ -24,25 +24,6 @@ class PreviewStage(CuraStage):
super().__init__(parent)
self._application = application
self._application.engineCreatedSignal.connect(self._engineCreated)
self._previously_active_view = None # type: Optional[View]
def onStageSelected(self) -> None:
"""When selecting the stage, remember which was the previous view so that
we can revert to that view when we go out of the stage later.
"""
self._previously_active_view = self._application.getController().getActiveView()
def onStageDeselected(self) -> None:
"""Called when going to a different stage (away from the Preview Stage).
When going to a different stage, the view should be reverted to what it
was before. Normally, that just reverts it to solid view.
"""
if self._previously_active_view is not None:
self._application.getController().setActiveView(self._previously_active_view.getPluginId())
self._previously_active_view = None
def _engineCreated(self) -> None:
"""Delayed load of the QML files.

View file

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

View file

@ -289,8 +289,9 @@ class SolidView(View):
def endRendering(self):
# check whether the xray overlay is showing badness
if time.time() > self._next_xray_checking_time\
and Application.getInstance().getPreferences().getValue(self._show_xray_warning_preference):
if (time.time() > self._next_xray_checking_time
and Application.getInstance().getPreferences().getValue(self._show_xray_warning_preference)
and self._xray_pass is not None):
self._next_xray_checking_time = time.time() + self._xray_checking_update_time
xray_img = self._xray_pass.getOutput()

View file

@ -1,6 +1,8 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtWidgets import QApplication
@ -35,6 +37,7 @@ class SupportEraser(Tool):
self._controller = self.getController()
self._selection_pass = None
self._picking_pass: Optional[PickingPass] = None
CuraApplication.getInstance().globalContainerStackChanged.connect(self._updateEnabled)
# Note: if the selection is cleared with this tool active, there is no way to switch to
@ -84,12 +87,13 @@ class SupportEraser(Tool):
# Only "normal" meshes can have anti_overhang_meshes added to them
return
# Create a pass for picking a world-space location from the mouse location
active_camera = self._controller.getScene().getActiveCamera()
picking_pass = PickingPass(active_camera.getViewportWidth(), active_camera.getViewportHeight())
picking_pass.render()
# Get the pass for picking a world-space location from the mouse location
if self._picking_pass is None:
self._picking_pass = Application.getInstance().getRenderer().getRenderPass("picking_selected")
if not self._picking_pass:
return
picked_position = picking_pass.getPickedPosition(event.x, event.y)
picked_position = self._picking_pass.getPickedPosition(event.x, event.y)
# Add the anti_overhang_mesh cube at the picked location
self._createEraserMesh(picked_node, picked_position)
@ -189,3 +193,6 @@ class SupportEraser(Tool):
mesh.calculateNormals()
return mesh
def getRequiredExtraRenderingPasses(self) -> list[str]:
return ["picking_selected"]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,7 +12,10 @@ 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

View file

@ -12,7 +12,10 @@ 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

View file

@ -38,7 +38,7 @@ Cura.ExpandablePopup
{
if (activeView == null)
{
UM.Controller.setActiveView(viewModel.getItem(0).id)
UM.Controller.activeStage.setActiveView(viewModel.getItem(0).id)
}
}
@ -110,7 +110,7 @@ Cura.ExpandablePopup
onClicked:
{
toggleContent()
UM.Controller.setActiveView(id)
UM.Controller.activeStage.setActiveView(id)
}
}
}

View file

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

View file

@ -13,7 +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
support_structure = tree
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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,12 +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

View file

@ -13,12 +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

View file

@ -463,6 +463,8 @@
"layerview_support_infill": [0, 230, 230, 127],
"layerview_move_combing": [0, 0, 255, 255],
"layerview_move_retraction": [128, 127, 255, 255],
"layerview_move_while_retracting": [127, 255, 255, 255],
"layerview_move_while_unretracting": [255, 127, 255, 255],
"layerview_support_interface": [63, 127, 255, 127],
"layerview_prime_tower": [0, 255, 255, 255],
"layerview_nozzle": [224, 192, 16, 64],