From eec792db0ac697345cf45828e34591785268d9a2 Mon Sep 17 00:00:00 2001 From: Asterchades Date: Thu, 28 Nov 2024 16:08:37 +1000 Subject: [PATCH 001/159] Update fdmprinter.def.json Set global Top and Bottom Layer values to match previous Ultimaker defaults. --- resources/definitions/fdmprinter.def.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index c930a624d0..a4adfd1d83 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -1595,7 +1595,7 @@ "maximum_value": "999999", "type": "int", "minimum_value_warning": "2", - "value": "0 if infill_sparse_density == 100 else math.ceil(round(top_thickness / resolveOrValue('layer_height'), 4))", + "value": "math.ceil(round(top_thickness / resolveOrValue('layer_height'), 4))" "limit_to_extruder": "top_bottom_extruder_nr", "settable_per_mesh": true } @@ -1625,7 +1625,7 @@ "default_value": 6, "maximum_value": "999999", "type": "int", - "value": "999999 if infill_sparse_density == 100 and not magic_spiralize else math.ceil(round(bottom_thickness / resolveOrValue('layer_height'), 4))", + "value": "math.ceil(round(bottom_thickness / resolveOrValue('layer_height'), 4))" "limit_to_extruder": "top_bottom_extruder_nr", "settable_per_mesh": true }, @@ -9193,4 +9193,4 @@ } } } -} \ No newline at end of file +} From e0cda11b811c4a704289661852549a9f847db636 Mon Sep 17 00:00:00 2001 From: Asterchades Date: Thu, 28 Nov 2024 16:09:20 +1000 Subject: [PATCH 002/159] Update ultimaker.def.json Removed Top and Bottom Layers values. With global defaults updated it is no longer required to re-set them. --- resources/definitions/ultimaker.def.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/resources/definitions/ultimaker.def.json b/resources/definitions/ultimaker.def.json index 65ceaf6f0f..b1a1cf87b1 100644 --- a/resources/definitions/ultimaker.def.json +++ b/resources/definitions/ultimaker.def.json @@ -16,7 +16,6 @@ { "acceleration_layer_0": { "value": "acceleration_topbottom" }, "acceleration_travel_enabled": { "value": false }, - "bottom_layers": { "value": "math.ceil(round(bottom_thickness / resolveOrValue('layer_height'), 4))" }, "bridge_enable_more_layers": { "value": false }, "bridge_fan_speed": { "value": "cool_fan_speed_max" }, "bridge_fan_speed_2": { "value": "cool_fan_speed_min" }, @@ -131,7 +130,6 @@ "support_wall_count": { "value": "1 if support_structure == 'tree' else 0" }, "support_xy_distance_overhang": { "value": "0.2" }, "support_z_distance": { "value": "0" }, - "top_layers": { "value": "math.ceil(round(top_thickness / resolveOrValue('layer_height'), 4))" }, "wall_0_material_flow_layer_0": { "value": "1.10 * material_flow_layer_0" }, "wall_thickness": { "value": "wall_line_width_0 + wall_line_width_x" }, "wall_x_material_flow_layer_0": { "value": "0.95 * material_flow_layer_0" }, @@ -141,4 +139,4 @@ "z_seam_relative": { "value": "True" }, "zig_zaggify_support": { "value": true } } -} \ No newline at end of file +} From cabd4261c2bd5ecc63971175d50c6656be570a03 Mon Sep 17 00:00:00 2001 From: Asterchades Date: Thu, 28 Nov 2024 16:10:58 +1000 Subject: [PATCH 003/159] Update fdmprinter.def.json Restore missing commas. Going to need those. --- resources/definitions/fdmprinter.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index a4adfd1d83..64e3c7d963 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -1595,7 +1595,7 @@ "maximum_value": "999999", "type": "int", "minimum_value_warning": "2", - "value": "math.ceil(round(top_thickness / resolveOrValue('layer_height'), 4))" + "value": "math.ceil(round(top_thickness / resolveOrValue('layer_height'), 4))", "limit_to_extruder": "top_bottom_extruder_nr", "settable_per_mesh": true } @@ -1625,7 +1625,7 @@ "default_value": 6, "maximum_value": "999999", "type": "int", - "value": "math.ceil(round(bottom_thickness / resolveOrValue('layer_height'), 4))" + "value": "math.ceil(round(bottom_thickness / resolveOrValue('layer_height'), 4))", "limit_to_extruder": "top_bottom_extruder_nr", "settable_per_mesh": true }, From a0d2d6fb620eecc2d56b05666f40b7b1f9ce6fec Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 28 Jan 2025 09:02:10 +0100 Subject: [PATCH 004/159] Add setting for retract/unretract during travel move CURA-11978 --- resources/definitions/fdmprinter.def.json | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index c1392e57ba..2425df01b1 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -4278,6 +4278,32 @@ "settable_per_mesh": false, "settable_per_extruder": true }, + "retraction_during_travel_ratio": + { + "label": "Retraction During Travel Move", + "description": "The ratio of retraction performed during the travel move, with the remainder completed while the nozzle is stationary, before traveling
  • When 0, the entire retraction is performed while stationary, before the travel begins
  • When 100, the entire retraction is performed during the travel move, bypassing the stationary phase
", + "unit": "%", + "type": "float", + "default_value": 0, + "minimum_value": 0, + "maximum_value": 100, + "enabled": "retraction_enable and machine_gcode_flavor != \"UltiGCode\"", + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "prime_during_travel_ratio": + { + "label": "Prime During Travel Move", + "description": "The ratio of priming performed during the travel move, with the remainder completed while the nozzle is stationary, after traveling
  • When 0, the entire priming is performed while stationary, after the travel ends
  • When 100, the entire priming is performed during the travel move, allowing the print to start immediately
", + "unit": "%", + "type": "float", + "default_value": 0, + "minimum_value": 0, + "maximum_value": 100, + "enabled": "retraction_enable and machine_gcode_flavor != \"UltiGCode\"", + "settable_per_mesh": false, + "settable_per_extruder": true + }, "retraction_speed": { "label": "Retraction Speed", From 850228498792684a4b282d40dedeea52bbafbc26 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 5 Feb 2025 10:33:51 +0100 Subject: [PATCH 005/159] Disable retract during travel for printers that handle retraction CURA-11978 --- resources/definitions/fdmprinter.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 1323f93a73..cf43fde121 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -4300,7 +4300,7 @@ "default_value": 0, "minimum_value": 0, "maximum_value": 100, - "enabled": "retraction_enable and machine_gcode_flavor != \"UltiGCode\"", + "enabled": "retraction_enable and not machine_firmware_retract and machine_gcode_flavor != \"UltiGCode\" and machine_gcode_flavor != \"BFB\"", "settable_per_mesh": false, "settable_per_extruder": true }, @@ -4313,7 +4313,7 @@ "default_value": 0, "minimum_value": 0, "maximum_value": 100, - "enabled": "retraction_enable and machine_gcode_flavor != \"UltiGCode\"", + "enabled": "retraction_enable and not machine_firmware_retract and machine_gcode_flavor != \"UltiGCode\" and machine_gcode_flavor != \"BFB\"", "settable_per_mesh": false, "settable_per_extruder": true }, From 3adf94cffb0450bc83ddf9aee4853153651c5910 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Fri, 14 Feb 2025 10:59:38 +0100 Subject: [PATCH 006/159] move settings to experimental category --- resources/definitions/fdmprinter.def.json | 52 +++++++++++------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index cf43fde121..48342b2eb1 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -4291,32 +4291,6 @@ "settable_per_mesh": false, "settable_per_extruder": true }, - "retraction_during_travel_ratio": - { - "label": "Retraction During Travel Move", - "description": "The ratio of retraction performed during the travel move, with the remainder completed while the nozzle is stationary, before traveling
  • When 0, the entire retraction is performed while stationary, before the travel begins
  • When 100, the entire retraction is performed during the travel move, bypassing the stationary phase
", - "unit": "%", - "type": "float", - "default_value": 0, - "minimum_value": 0, - "maximum_value": 100, - "enabled": "retraction_enable and not machine_firmware_retract and machine_gcode_flavor != \"UltiGCode\" and machine_gcode_flavor != \"BFB\"", - "settable_per_mesh": false, - "settable_per_extruder": true - }, - "prime_during_travel_ratio": - { - "label": "Prime During Travel Move", - "description": "The ratio of priming performed during the travel move, with the remainder completed while the nozzle is stationary, after traveling
  • When 0, the entire priming is performed while stationary, after the travel ends
  • When 100, the entire priming is performed during the travel move, allowing the print to start immediately
", - "unit": "%", - "type": "float", - "default_value": 0, - "minimum_value": 0, - "maximum_value": 100, - "enabled": "retraction_enable and not machine_firmware_retract and machine_gcode_flavor != \"UltiGCode\" and machine_gcode_flavor != \"BFB\"", - "settable_per_mesh": false, - "settable_per_extruder": true - }, "retraction_speed": { "label": "Retraction Speed", @@ -9015,6 +8989,32 @@ "default_value": true, "settable_per_mesh": true }, + "retraction_during_travel_ratio": + { + "label": "Retraction During Travel Move", + "description": "The ratio of retraction performed during the travel move, with the remainder completed while the nozzle is stationary, before traveling
  • When 0, the entire retraction is performed while stationary, before the travel begins
  • When 100, the entire retraction is performed during the travel move, bypassing the stationary phase
", + "unit": "%", + "type": "float", + "default_value": 0, + "minimum_value": 0, + "maximum_value": 100, + "enabled": "retraction_enable and not machine_firmware_retract and machine_gcode_flavor != \"UltiGCode\" and machine_gcode_flavor != \"BFB\"", + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "prime_during_travel_ratio": + { + "label": "Prime During Travel Move", + "description": "The ratio of priming performed during the travel move, with the remainder completed while the nozzle is stationary, after traveling
  • When 0, the entire priming is performed while stationary, after the travel ends
  • When 100, the entire priming is performed during the travel move, allowing the print to start immediately
", + "unit": "%", + "type": "float", + "default_value": 0, + "minimum_value": 0, + "maximum_value": 100, + "enabled": "retraction_enable and not machine_firmware_retract and machine_gcode_flavor != \"UltiGCode\" and machine_gcode_flavor != \"BFB\"", + "settable_per_mesh": false, + "settable_per_extruder": true + }, "scarf_joint_seam_length": { "label": "Scarf Seam Length", From cf52772fd19043d20fe2110ef4a539172ff32238 Mon Sep 17 00:00:00 2001 From: Jeremy Salwen Date: Fri, 21 Mar 2025 08:42:43 -0400 Subject: [PATCH 007/159] Enable more retraction settings to be set per-model These retraction settings were already supported on a per-object basis on the backend by https://github.com/Ultimaker/CuraEngine/pull/1769, but were not enabled by the corresponding frontend PR at the time. This helps get incrementally closer to exposing per-model settings https://github.com/Ultimaker/Cura/issues/3193 --- resources/definitions/fdmprinter.def.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index b42b5c417a..ce43289636 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -4553,7 +4553,7 @@ "minimum_value_warning": "-0.0001", "maximum_value_warning": "10.0", "enabled": "retraction_enable and machine_gcode_flavor != \"UltiGCode\"", - "settable_per_mesh": false, + "settable_per_mesh": true, "settable_per_extruder": true }, "retraction_speed": @@ -4643,7 +4643,7 @@ "maximum_value": 999999999, "type": "int", "enabled": "retraction_enable", - "settable_per_mesh": false, + "settable_per_mesh": true, "settable_per_extruder": true }, "retraction_extrusion_window": @@ -4657,7 +4657,7 @@ "maximum_value_warning": "retraction_amount * 2", "value": "retraction_amount", "enabled": "retraction_enable", - "settable_per_mesh": false, + "settable_per_mesh": true, "settable_per_extruder": true }, "retraction_combing": From 93694e2da4090103ce121621efd0fa0a6b978882 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 20 May 2025 15:51:13 +0200 Subject: [PATCH 008/159] W.I.P.: Add paint-shader/layer so it can be used for the UV-painting feature. Currently replacing the 'disabled' batch until we can get it to switch out on command (when we have the painting stage/tool/... pluging up and running. part of CURA-12543 --- plugins/SolidView/SolidView.py | 27 +++++-- resources/shaders/paint.shader | 142 +++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 6 deletions(-) create mode 100644 resources/shaders/paint.shader diff --git a/plugins/SolidView/SolidView.py b/plugins/SolidView/SolidView.py index 7f32b0df7f..1f2dabdbdd 100644 --- a/plugins/SolidView/SolidView.py +++ b/plugins/SolidView/SolidView.py @@ -1,13 +1,12 @@ # Copyright (c) 2021 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -import os.path from UM.View.View import View from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Selection import Selection from UM.Resources import Resources -from PyQt6.QtGui import QOpenGLContext, QDesktopServices, QImage -from PyQt6.QtCore import QSize, QUrl +from PyQt6.QtGui import QDesktopServices, QImage +from PyQt6.QtCore import QUrl import numpy as np import time @@ -36,16 +35,20 @@ class SolidView(View): """Standard view for mesh models.""" _show_xray_warning_preference = "view/show_xray_warning" + _show_overhang_preference = "view/show_overhang" + _paint_active_preference = "view/paint_active" def __init__(self): super().__init__() application = Application.getInstance() - application.getPreferences().addPreference("view/show_overhang", True) + application.getPreferences().addPreference(self._show_overhang_preference, True) + application.getPreferences().addPreference(self._paint_active_preference, False) application.globalContainerStackChanged.connect(self._onGlobalContainerChanged) self._enabled_shader = None self._disabled_shader = None self._non_printing_shader = None self._support_mesh_shader = None + self._paint_shader = None self._xray_shader = None self._xray_pass = None @@ -139,6 +142,11 @@ class SolidView(View): min_height = max(min_height, init_layer_height) return min_height + def _setPaintTexture(self): + self._paint_texture = OpenGL.getInstance().createTexture(256, 256) + if self._paint_shader: + self._paint_shader.setTexture(0, self._paint_texture) + def _checkSetup(self): if not self._extruders_model: self._extruders_model = Application.getInstance().getExtrudersModel() @@ -167,6 +175,10 @@ class SolidView(View): self._support_mesh_shader.setUniformValue("u_vertical_stripes", True) self._support_mesh_shader.setUniformValue("u_width", 5.0) + if not self._paint_shader: + self._paint_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "paint.shader")) + self._setPaintTexture() + if not Application.getInstance().getPreferences().getValue(self._show_xray_warning_preference): self._xray_shader = None self._xray_composite_shader = None @@ -204,6 +216,9 @@ class SolidView(View): self._old_composite_shader = self._composite_pass.getCompositeShader() self._composite_pass.setCompositeShader(self._xray_composite_shader) + def setUvPixel(self, x, y, color): + self._paint_texture.setPixel(x, y, color) + def beginRendering(self): scene = self.getController().getScene() renderer = self.getRenderer() @@ -212,7 +227,7 @@ class SolidView(View): global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack: - if Application.getInstance().getPreferences().getValue("view/show_overhang"): + if Application.getInstance().getPreferences().getValue(self._show_overhang_preference): # Make sure the overhang angle is valid before passing it to the shader if self._support_angle >= 0 and self._support_angle <= 90: self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(90 - self._support_angle))) @@ -221,7 +236,7 @@ class SolidView(View): else: self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0))) self._enabled_shader.setUniformValue("u_lowestPrintableHeight", self._lowest_printable_height) - disabled_batch = renderer.createRenderBatch(shader = self._disabled_shader) + disabled_batch = renderer.createRenderBatch(shader = self._paint_shader) #### TODO: put back to 'self._disabled_shader' normal_object_batch = renderer.createRenderBatch(shader = self._enabled_shader) renderer.addRenderBatch(disabled_batch) renderer.addRenderBatch(normal_object_batch) diff --git a/resources/shaders/paint.shader b/resources/shaders/paint.shader new file mode 100644 index 0000000000..83682c7222 --- /dev/null +++ b/resources/shaders/paint.shader @@ -0,0 +1,142 @@ +[shaders] +vertex = + uniform highp mat4 u_modelMatrix; + uniform highp mat4 u_viewMatrix; + uniform highp mat4 u_projectionMatrix; + + uniform highp mat4 u_normalMatrix; + + attribute highp vec4 a_vertex; + attribute highp vec4 a_normal; + attribute highp vec2 a_uvs; + + varying highp vec3 v_vertex; + varying highp vec3 v_normal; + varying highp vec2 v_uvs; + + void main() + { + vec4 world_space_vert = u_modelMatrix * a_vertex; + gl_Position = u_projectionMatrix * u_viewMatrix * world_space_vert; + + v_vertex = world_space_vert.xyz; + v_normal = (u_normalMatrix * normalize(a_normal)).xyz; + + v_uvs = a_uvs; + } + +fragment = + uniform mediump vec4 u_ambientColor; + uniform mediump vec4 u_diffuseColor; + uniform highp vec3 u_lightPosition; + uniform highp vec3 u_viewPosition; + uniform mediump float u_opacity; + uniform sampler2D u_texture; + + varying highp vec3 v_vertex; + varying highp vec3 v_normal; + varying highp vec2 v_uvs; + + void main() + { + mediump vec4 final_color = vec4(0.0); + + /* Ambient Component */ + final_color += u_ambientColor; + + highp vec3 normal = normalize(v_normal); + highp vec3 light_dir = normalize(u_lightPosition - v_vertex); + + /* Diffuse Component */ + highp float n_dot_l = clamp(dot(normal, light_dir), 0.0, 1.0); + final_color += (n_dot_l * u_diffuseColor); + + final_color.a = u_opacity; + + lowp vec4 texture = texture2D(u_texture, v_uvs); + final_color = mix(final_color, texture, texture.a); + + gl_FragColor = final_color; + } + +vertex41core = + #version 410 + uniform highp mat4 u_modelMatrix; + uniform highp mat4 u_viewMatrix; + uniform highp mat4 u_projectionMatrix; + + uniform highp mat4 u_normalMatrix; + + in highp vec4 a_vertex; + in highp vec4 a_normal; + in highp vec2 a_uvs; + + out highp vec3 v_vertex; + out highp vec3 v_normal; + out highp vec2 v_uvs; + + void main() + { + vec4 world_space_vert = u_modelMatrix * a_vertex; + gl_Position = u_projectionMatrix * u_viewMatrix * world_space_vert; + + v_vertex = world_space_vert.xyz; + v_normal = (u_normalMatrix * normalize(a_normal)).xyz; + + v_uvs = a_uvs; + } + +fragment41core = + #version 410 + uniform mediump vec4 u_ambientColor; + uniform mediump vec4 u_diffuseColor; + uniform highp vec3 u_lightPosition; + uniform highp vec3 u_viewPosition; + uniform mediump float u_opacity; + uniform sampler2D u_texture; + + in highp vec3 v_vertex; + in highp vec3 v_normal; + in highp vec2 v_uvs; + out vec4 frag_color; + + void main() + { + mediump vec4 final_color = vec4(0.0); + + /* Ambient Component */ + final_color += u_ambientColor; + + highp vec3 normal = normalize(v_normal); + highp vec3 light_dir = normalize(u_lightPosition - v_vertex); + + /* Diffuse Component */ + highp float n_dot_l = clamp(dot(normal, light_dir), 0.0, 1.0); + final_color += (n_dot_l * u_diffuseColor); + + final_color.a = u_opacity; + + lowp vec4 texture = texture(u_texture, v_uvs); + final_color = mix(final_color, texture, texture.a); + + frag_color = final_color; + } + +[defaults] +u_ambientColor = [0.3, 0.3, 0.3, 1.0] +u_diffuseColor = [1.0, 1.0, 1.0, 1.0] +u_opacity = 0.5 +u_texture = 0 + +[bindings] +u_modelMatrix = model_matrix +u_viewMatrix = view_matrix +u_projectionMatrix = projection_matrix +u_normalMatrix = normal_matrix +u_lightPosition = light_0_position +u_viewPosition = camera_position + +[attributes] +a_vertex = vertex +a_normal = normal +a_uvs = uv0 From 19ea88a8ce474968b53b06f992cccd5964f58fd6 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 20 May 2025 15:56:21 +0200 Subject: [PATCH 009/159] W.I.P. Start of paint-tool plugin UX work. Should be able to paint pixels now if the tools is active, and the model loaded is with UV-coords (that rules out our current impl. of 3MF at the moment -- use OBJ instead), and you position the model outside of the build-plate so the paint-shadr that is temporarily replacing the 'disabled' one is showing. Will need a lot of extra features and optimizations still! part of CURA-12543 --- plugins/PaintTool/PaintTool.py | 135 ++++++++++++++++++++++++++++++++ plugins/PaintTool/PaintTool.qml | 14 ++++ plugins/PaintTool/__init__.py | 21 +++++ plugins/PaintTool/plugin.json | 8 ++ 4 files changed, 178 insertions(+) create mode 100644 plugins/PaintTool/PaintTool.py create mode 100644 plugins/PaintTool/PaintTool.qml create mode 100644 plugins/PaintTool/__init__.py create mode 100644 plugins/PaintTool/plugin.json diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py new file mode 100644 index 0000000000..d7a70581fb --- /dev/null +++ b/plugins/PaintTool/PaintTool.py @@ -0,0 +1,135 @@ +# Copyright (c) 2025 UltiMaker +# Cura is released under the terms of the LGPLv3 or higher. + +from typing import cast, Optional + +import numpy +from PyQt6.QtCore import Qt + +from UM.Application import Application +from UM.Event import Event, MouseEvent, KeyEvent +from UM.Tool import Tool +from cura.PickingPass import PickingPass + + +class PaintTool(Tool): + """Provides the tool to paint meshes. + """ + + def __init__(self) -> None: + super().__init__() + + self._shortcut_key = Qt.Key.Key_P + + """ + # CURA-5966 Make sure to render whenever objects get selected/deselected. + Selection.selectionChanged.connect(self.propertyChanged) + """ + + @staticmethod + def _get_intersect_ratio_via_pt(a, pt, b, c): + # compute the intersection of (param) A - pt with (param) B - (param) C + + # compute unit vectors of directions of lines A and B + udir_a = a - pt + udir_a /= numpy.linalg.norm(udir_a) + udir_b = b - c + udir_b /= numpy.linalg.norm(udir_b) + + # find unit direction vector for line C, which is perpendicular to lines A and B + udir_res = numpy.cross(udir_b, udir_a) + udir_res /= numpy.linalg.norm(udir_res) + + # solve system of equations + rhs = b - a + lhs = numpy.array([udir_a, -udir_b, udir_res]).T + solved = numpy.linalg.solve(lhs, rhs) + + # get the ratio + intersect = ((a + solved[0] * udir_a) + (b + solved[1] * udir_b)) * 0.5 + return numpy.linalg.norm(pt - intersect) / numpy.linalg.norm(a - intersect) + + def event(self, event: Event) -> bool: + """Handle mouse and keyboard events. + + :param event: The event to handle. + :return: Whether this event has been caught by this tool (True) or should + be passed on (False). + """ + super().event(event) + + # Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes + if event.type == Event.ToolActivateEvent: + return False + + if event.type == Event.ToolDeactivateEvent: + return False + + if event.type == Event.KeyPressEvent and cast(KeyEvent, event).key == KeyEvent.ShiftKey: + return False + + if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(): + if MouseEvent.LeftButton not in cast(MouseEvent, event).buttons: + return False + if not self._selection_pass: + return False + + camera = self._controller.getScene().getActiveCamera() + if not camera: + return False + + evt = cast(MouseEvent, event) + + ppass = PickingPass(self._selection_pass._width, self._selection_pass._height) + ppass.render() + pt = ppass.getPickedPosition(evt.x, evt.y).getData() + + self._selection_pass._renderObjectsMode() # TODO: <- Fix this! + + node_id = self._selection_pass.getIdAtPosition(evt.x, evt.y) + if node_id is None: + return False + node = Application.getInstance().getController().getScene().findObject(node_id) + if node is None: + return False + + self._selection_pass._renderFacesMode() # TODO: <- Fix this! + + face_id = self._selection_pass.getFaceIdAtPosition(evt.x, evt.y) + if face_id < 0: + return False + + meshdata = node.getMeshDataTransformed() # TODO: <- don't forget to optimize, if the mesh hasn't changed (transforms) then it should be reused! + if not meshdata: + return False + + va, vb, vc = meshdata.getFaceNodes(face_id) + ta, tb, tc = node.getMeshData().getFaceUvCoords(face_id) + + # 'Weight' of each vertex that would produce point pt, so we can generate the texture coordinates from the uv ones of the vertices. + # See (also) https://mathworld.wolfram.com/BarycentricCoordinates.html + wa = PaintTool._get_intersect_ratio_via_pt(va, pt, vb, vc) + wb = PaintTool._get_intersect_ratio_via_pt(vb, pt, vc, va) + wc = PaintTool._get_intersect_ratio_via_pt(vc, pt, va, vb) + wt = wa + wb + wc + wa /= wt + wb /= wt + wc /= wt + texcoords = wa * ta + wb * tb + wc * tc + + solidview = Application.getInstance().getController().getActiveView() + if solidview.getPluginId() != "SolidView": + return False + + solidview.setUvPixel(texcoords[0], texcoords[1], [255, 128, 0, 255]) + + return True + + if event.type == Event.MouseMoveEvent: + evt = cast(MouseEvent, event) + return False #True + + if event.type == Event.MouseReleaseEvent: + return False #True + + return False diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml new file mode 100644 index 0000000000..2e634790c2 --- /dev/null +++ b/plugins/PaintTool/PaintTool.qml @@ -0,0 +1,14 @@ +// Copyright (c) 2025 UltiMaker +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.2 + +import UM 1.7 as UM + +Item +{ + id: base + width: childrenRect.width + height: childrenRect.height + UM.I18nCatalog { id: catalog; name: "cura"} +} diff --git a/plugins/PaintTool/__init__.py b/plugins/PaintTool/__init__.py new file mode 100644 index 0000000000..38eac5bc45 --- /dev/null +++ b/plugins/PaintTool/__init__.py @@ -0,0 +1,21 @@ +# Copyright (c) 2025 UltiMaker +# Cura is released under the terms of the LGPLv3 or higher. + +from . import PaintTool + +from UM.i18n import i18nCatalog +i18n_catalog = i18nCatalog("cura") + +def getMetaData(): + return { + "tool": { + "name": i18n_catalog.i18nc("@action:button", "Paint"), + "description": i18n_catalog.i18nc("@info:tooltip", "Paint Model"), + "icon": "Visual", + "tool_panel": "PaintTool.qml", + "weight": 0 + } + } + +def register(app): + return { "tool": PaintTool.PaintTool() } diff --git a/plugins/PaintTool/plugin.json b/plugins/PaintTool/plugin.json new file mode 100644 index 0000000000..2a55d677d2 --- /dev/null +++ b/plugins/PaintTool/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "Paint Tools", + "author": "UltiMaker", + "version": "1.0.0", + "description": "Provides the paint tools.", + "api": 8, + "i18n-catalog": "cura" +} From c5592eea83ec188bf6a8f5956c304f22f7c396b5 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 21 May 2025 15:19:07 +0200 Subject: [PATCH 010/159] Slightly optimize and refactor the w.i.p. paint-tool. Just enought so that the truly ugly things are out of it. part of CURA-12543 --- plugins/PaintTool/PaintTool.py | 47 ++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index d7a70581fb..3ee94db1d9 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -8,6 +8,7 @@ from PyQt6.QtCore import Qt from UM.Application import Application from UM.Event import Event, MouseEvent, KeyEvent +from UM.Scene.Selection import Selection from UM.Tool import Tool from cura.PickingPass import PickingPass @@ -21,10 +22,9 @@ class PaintTool(Tool): self._shortcut_key = Qt.Key.Key_P - """ - # CURA-5966 Make sure to render whenever objects get selected/deselected. - Selection.selectionChanged.connect(self.propertyChanged) - """ + self._node_cache = None + self._mesh_transformed_cache = None + self._cache_dirty = True @staticmethod def _get_intersect_ratio_via_pt(a, pt, b, c): @@ -49,6 +49,9 @@ class PaintTool(Tool): intersect = ((a + solved[0] * udir_a) + (b + solved[1] * udir_b)) * 0.5 return numpy.linalg.norm(pt - intersect) / numpy.linalg.norm(a - intersect) + def _nodeTransformChanged(self, *args) -> None: + self._cache_dirty = True + def event(self, event: Event) -> bool: """Handle mouse and keyboard events. @@ -78,32 +81,33 @@ class PaintTool(Tool): if not camera: return False - evt = cast(MouseEvent, event) - - ppass = PickingPass(self._selection_pass._width, self._selection_pass._height) - ppass.render() - pt = ppass.getPickedPosition(evt.x, evt.y).getData() - - self._selection_pass._renderObjectsMode() # TODO: <- Fix this! - - node_id = self._selection_pass.getIdAtPosition(evt.x, evt.y) - if node_id is None: - return False - node = Application.getInstance().getController().getScene().findObject(node_id) + node = Selection.getAllSelectedObjects()[0] if node is None: return False - self._selection_pass._renderFacesMode() # TODO: <- Fix this! + if node != self._node_cache: + if self._node_cache is not None: + self._node_cache.transformationChanged.disconnect(self._nodeTransformChanged) + self._node_cache = node + self._node_cache.transformationChanged.connect(self._nodeTransformChanged) + if self._cache_dirty: + self._cache_dirty = False + self._mesh_transformed_cache = self._node_cache.getMeshDataTransformed() + if not self._mesh_transformed_cache: + return False + evt = cast(MouseEvent, event) + + self._selection_pass.renderFacesMode() face_id = self._selection_pass.getFaceIdAtPosition(evt.x, evt.y) if face_id < 0: return False - meshdata = node.getMeshDataTransformed() # TODO: <- don't forget to optimize, if the mesh hasn't changed (transforms) then it should be reused! - if not meshdata: - return False + ppass = PickingPass(camera.getViewportWidth(), camera.getViewportHeight()) + ppass.render() + pt = ppass.getPickedPosition(evt.x, evt.y).getData() - va, vb, vc = meshdata.getFaceNodes(face_id) + va, vb, vc = self._mesh_transformed_cache.getFaceNodes(face_id) ta, tb, tc = node.getMeshData().getFaceUvCoords(face_id) # 'Weight' of each vertex that would produce point pt, so we can generate the texture coordinates from the uv ones of the vertices. @@ -120,7 +124,6 @@ class PaintTool(Tool): solidview = Application.getInstance().getController().getActiveView() if solidview.getPluginId() != "SolidView": return False - solidview.setUvPixel(texcoords[0], texcoords[1], [255, 128, 0, 255]) return True From 3ae85e3e2aee73d60f068f0286c9720e71338d46 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 21 May 2025 21:50:17 +0200 Subject: [PATCH 011/159] Refactored paint-view into its own thing. part of CURA-12543 --- plugins/PaintTool/PaintTool.py | 18 +++++--- plugins/PaintTool/PaintView.py | 42 +++++++++++++++++++ plugins/PaintTool/__init__.py | 11 ++++- .../PaintTool}/paint.shader | 0 plugins/SolidView/SolidView.py | 16 +------ 5 files changed, 65 insertions(+), 22 deletions(-) create mode 100644 plugins/PaintTool/PaintView.py rename {resources/shaders => plugins/PaintTool}/paint.shader (100%) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index 3ee94db1d9..2d204ceff9 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -27,7 +27,7 @@ class PaintTool(Tool): self._cache_dirty = True @staticmethod - def _get_intersect_ratio_via_pt(a, pt, b, c): + def _get_intersect_ratio_via_pt(a, pt, b, c) -> float: # compute the intersection of (param) A - pt with (param) B - (param) C # compute unit vectors of directions of lines A and B @@ -61,12 +61,18 @@ class PaintTool(Tool): """ super().event(event) + controller = Application.getInstance().getController() + # Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes if event.type == Event.ToolActivateEvent: - return False + 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: - return False + controller.setActiveStage("PrepareStage") + controller.setActiveView("SolidView") + return True if event.type == Event.KeyPressEvent and cast(KeyEvent, event).key == KeyEvent.ShiftKey: return False @@ -121,10 +127,10 @@ class PaintTool(Tool): wc /= wt texcoords = wa * ta + wb * tb + wc * tc - solidview = Application.getInstance().getController().getActiveView() - if solidview.getPluginId() != "SolidView": + paintview = controller.getActiveView() + if paintview.getPluginId() != "PaintTool": return False - solidview.setUvPixel(texcoords[0], texcoords[1], [255, 128, 0, 255]) + paintview.setUvPixel(texcoords[0], texcoords[1], [255, 128, 0, 255]) return True diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py new file mode 100644 index 0000000000..31a1a7f5f6 --- /dev/null +++ b/plugins/PaintTool/PaintView.py @@ -0,0 +1,42 @@ +# Copyright (c) 2025 UltiMaker +# Cura is released under the terms of the LGPLv3 or higher. +import os + +from UM.PluginRegistry import PluginRegistry +from UM.View.View import View +from UM.Scene.Selection import Selection +from UM.View.GL.OpenGL import OpenGL +from UM.i18n import i18nCatalog + +catalog = i18nCatalog("cura") + + +class PaintView(View): + """View for model-painting.""" + + def __init__(self) -> None: + super().__init__() + self._paint_shader = None + self._paint_texture = None + + 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) + if not self._paint_texture: + self._paint_texture = OpenGL.getInstance().createTexture(256, 256) + self._paint_shader.setTexture(0, self._paint_texture) + + def setUvPixel(self, x, y, color) -> None: + self._paint_texture.setPixel(x, y, color) + + def beginRendering(self) -> None: + renderer = self.getRenderer() + self._checkSetup() + paint_batch = renderer.createRenderBatch(shader=self._paint_shader) + renderer.addRenderBatch(paint_batch) + + node = Selection.getAllSelectedObjects()[0] + if node is None: + return + paint_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData(), normal_transformation=node.getCachedNormalMatrix()) diff --git a/plugins/PaintTool/__init__.py b/plugins/PaintTool/__init__.py index 38eac5bc45..301bc49e0d 100644 --- a/plugins/PaintTool/__init__.py +++ b/plugins/PaintTool/__init__.py @@ -2,6 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. from . import PaintTool +from . import PaintView from UM.i18n import i18nCatalog i18n_catalog = i18nCatalog("cura") @@ -14,8 +15,16 @@ def getMetaData(): "icon": "Visual", "tool_panel": "PaintTool.qml", "weight": 0 + }, + "view": { + "name": i18n_catalog.i18nc("@item:inmenu", "Paint view"), + "weight": 0, + "visible": False } } def register(app): - return { "tool": PaintTool.PaintTool() } + return { + "tool": PaintTool.PaintTool(), + "view": PaintView.PaintView() + } diff --git a/resources/shaders/paint.shader b/plugins/PaintTool/paint.shader similarity index 100% rename from resources/shaders/paint.shader rename to plugins/PaintTool/paint.shader diff --git a/plugins/SolidView/SolidView.py b/plugins/SolidView/SolidView.py index 1f2dabdbdd..e115267720 100644 --- a/plugins/SolidView/SolidView.py +++ b/plugins/SolidView/SolidView.py @@ -42,13 +42,11 @@ class SolidView(View): super().__init__() application = Application.getInstance() application.getPreferences().addPreference(self._show_overhang_preference, True) - application.getPreferences().addPreference(self._paint_active_preference, False) application.globalContainerStackChanged.connect(self._onGlobalContainerChanged) self._enabled_shader = None self._disabled_shader = None self._non_printing_shader = None self._support_mesh_shader = None - self._paint_shader = None self._xray_shader = None self._xray_pass = None @@ -142,11 +140,6 @@ class SolidView(View): min_height = max(min_height, init_layer_height) return min_height - def _setPaintTexture(self): - self._paint_texture = OpenGL.getInstance().createTexture(256, 256) - if self._paint_shader: - self._paint_shader.setTexture(0, self._paint_texture) - def _checkSetup(self): if not self._extruders_model: self._extruders_model = Application.getInstance().getExtrudersModel() @@ -175,10 +168,6 @@ class SolidView(View): self._support_mesh_shader.setUniformValue("u_vertical_stripes", True) self._support_mesh_shader.setUniformValue("u_width", 5.0) - if not self._paint_shader: - self._paint_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "paint.shader")) - self._setPaintTexture() - if not Application.getInstance().getPreferences().getValue(self._show_xray_warning_preference): self._xray_shader = None self._xray_composite_shader = None @@ -216,9 +205,6 @@ class SolidView(View): self._old_composite_shader = self._composite_pass.getCompositeShader() self._composite_pass.setCompositeShader(self._xray_composite_shader) - def setUvPixel(self, x, y, color): - self._paint_texture.setPixel(x, y, color) - def beginRendering(self): scene = self.getController().getScene() renderer = self.getRenderer() @@ -236,7 +222,7 @@ class SolidView(View): else: self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0))) self._enabled_shader.setUniformValue("u_lowestPrintableHeight", self._lowest_printable_height) - disabled_batch = renderer.createRenderBatch(shader = self._paint_shader) #### TODO: put back to 'self._disabled_shader' + disabled_batch = renderer.createRenderBatch(shader = self._disabled_shader) normal_object_batch = renderer.createRenderBatch(shader = self._enabled_shader) renderer.addRenderBatch(disabled_batch) renderer.addRenderBatch(normal_object_batch) From a176957fa7b0f7e57ff47c372fbbeb37ad1e8d95 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Thu, 22 May 2025 10:14:00 +0200 Subject: [PATCH 012/159] Painting: Set color, brush-size, brush-shape. part of CURA-12543 --- plugins/PaintTool/PaintTool.py | 48 ++++++++- plugins/PaintTool/PaintTool.qml | 168 +++++++++++++++++++++++++++++++- plugins/PaintTool/PaintView.py | 7 +- 3 files changed, 219 insertions(+), 4 deletions(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index 2d204ceff9..5bd0c2187d 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -5,9 +5,11 @@ from typing import cast, Optional import numpy from PyQt6.QtCore import Qt +from typing import List, Tuple from UM.Application import Application from UM.Event import Event, MouseEvent, KeyEvent +from UM.Logger import Logger from UM.Scene.Selection import Selection from UM.Tool import Tool from cura.PickingPass import PickingPass @@ -26,6 +28,31 @@ class PaintTool(Tool): self._mesh_transformed_cache = None self._cache_dirty = True + self._color_str_to_rgba = { + "A": [192, 0, 192, 255], + "B": [232, 128, 0, 255], + "C": [0, 255, 0, 255], + "D": [255, 255, 255, 255], + } + + self._brush_size = 10 + self._brush_color = "A" + self._brush_shape = "A" + + def setPaintType(self, paint_type: str) -> None: + Logger.warning(f"TODO: Implement paint-types ({paint_type}).") + pass + + def setBrushSize(self, brush_size: float) -> None: + self._brush_size = int(brush_size) + print(self._brush_size) + + def setBrushColor(self, brush_color: str) -> None: + self._brush_color = brush_color + + def setBrushShape(self, brush_shape: str) -> None: + self._brush_shape = brush_shape + @staticmethod def _get_intersect_ratio_via_pt(a, pt, b, c) -> float: # compute the intersection of (param) A - pt with (param) B - (param) C @@ -52,6 +79,20 @@ class PaintTool(Tool): def _nodeTransformChanged(self, *args) -> None: self._cache_dirty = True + def _getBrushPixels(self, mid_x: float, mid_y: float, w: float, h: float) -> List[Tuple[float, float]]: + res = [] + include = False + for y in range(-self._brush_size, self._brush_size + 1): + for x in range(-self._brush_size, self._brush_size + 1): + match self._brush_shape: + case "A": + include = True + case "B": + include = x * x + y * y <= self._brush_size * self._brush_size + if include: + res.append((mid_x + (x / w), mid_y + (y / h))) + return res + def event(self, event: Event) -> bool: """Handle mouse and keyboard events. @@ -128,9 +169,12 @@ class PaintTool(Tool): texcoords = wa * ta + wb * tb + wc * tc paintview = controller.getActiveView() - if paintview.getPluginId() != "PaintTool": + if paintview is None or paintview.getPluginId() != "PaintTool": return False - paintview.setUvPixel(texcoords[0], texcoords[1], [255, 128, 0, 255]) + color = self._color_str_to_rgba[self._brush_color] + w, h = paintview.getUvTexDimensions() + for (x, y) in self._getBrushPixels(texcoords[0], texcoords[1], float(w), float(h)): + paintview.setUvPixel(x, y, color) return True diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml index 2e634790c2..902d5a700d 100644 --- a/plugins/PaintTool/PaintTool.qml +++ b/plugins/PaintTool/PaintTool.qml @@ -1,7 +1,8 @@ // Copyright (c) 2025 UltiMaker // Cura is released under the terms of the LGPLv3 or higher. -import QtQuick 2.2 +import QtQuick +import QtQuick.Layouts import UM 1.7 as UM @@ -11,4 +12,169 @@ Item width: childrenRect.width height: childrenRect.height UM.I18nCatalog { id: catalog; name: "cura"} + + ColumnLayout + { + RowLayout + { + UM.ToolbarButton + { + id: paintTypeA + + text: catalog.i18nc("@action:button", "Paint Type A") + toolItem: UM.ColorImage + { + source: UM.Theme.getIcon("Buildplate") + color: UM.Theme.getColor("icon") + } + property bool needBorder: true + + z: 2 + + onClicked: UM.Controller.triggerActionWithData("setPaintType", "A") + } + + UM.ToolbarButton + { + id: paintTypeB + + text: catalog.i18nc("@action:button", "Paint Type B") + toolItem: UM.ColorImage + { + source: UM.Theme.getIcon("BlackMagic") + color: UM.Theme.getColor("icon") + } + property bool needBorder: true + + z: 2 + + onClicked: UM.Controller.triggerActionWithData("setPaintType", "B") + } + } + + RowLayout + { + UM.ToolbarButton + { + id: colorButtonA + + text: catalog.i18nc("@action:button", "Color A") + toolItem: UM.ColorImage + { + source: UM.Theme.getIcon("Eye") + color: "purple" + } + property bool needBorder: true + + z: 2 + + onClicked: UM.Controller.triggerActionWithData("setBrushColor", "A") + } + + UM.ToolbarButton + { + id: colorButtonB + + text: catalog.i18nc("@action:button", "Color B") + toolItem: UM.ColorImage + { + source: UM.Theme.getIcon("Eye") + color: "orange" + } + property bool needBorder: true + + z: 2 + + onClicked: UM.Controller.triggerActionWithData("setBrushColor", "B") + } + + UM.ToolbarButton + { + id: colorButtonC + + text: catalog.i18nc("@action:button", "Color C") + toolItem: UM.ColorImage + { + source: UM.Theme.getIcon("Eye") + color: "green" + } + property bool needBorder: true + + z: 2 + + onClicked: UM.Controller.triggerActionWithData("setBrushColor", "C") + } + + UM.ToolbarButton + { + id: colorButtonD + + text: catalog.i18nc("@action:button", "Color D") + toolItem: UM.ColorImage + { + source: UM.Theme.getIcon("Eye") + color: "ghostwhite" + } + property bool needBorder: true + + z: 2 + + onClicked: UM.Controller.triggerActionWithData("setBrushColor", "D") + } + } + + RowLayout + { + UM.ToolbarButton + { + id: shapeSquareButton + + text: catalog.i18nc("@action:button", "Square Brush") + toolItem: UM.ColorImage + { + source: UM.Theme.getIcon("MeshTypeNormal") + color: UM.Theme.getColor("icon") + } + property bool needBorder: true + + z: 2 + + onClicked: UM.Controller.triggerActionWithData("setBrushShape", "A") + } + + UM.ToolbarButton + { + id: shapeCircleButton + + text: catalog.i18nc("@action:button", "Round Brush") + toolItem: UM.ColorImage + { + source: UM.Theme.getIcon("CircleOutline") + color: UM.Theme.getColor("icon") + } + property bool needBorder: true + + z: 2 + + onClicked: UM.Controller.triggerActionWithData("setBrushShape", "B") + } + + UM.Slider + { + id: shapeSizeSlider + + from: 1 + to: 50 + value: 10 + + onPressedChanged: function(pressed) + { + if(! pressed) + { + UM.Controller.triggerActionWithData("setBrushSize", shapeSizeSlider.value) + } + } + } + } + } } diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index 31a1a7f5f6..0923d007d7 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -18,18 +18,23 @@ class PaintView(View): super().__init__() self._paint_shader = None self._paint_texture = None + self._tex_width = 256 + self._tex_height = 256 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) if not self._paint_texture: - self._paint_texture = OpenGL.getInstance().createTexture(256, 256) + self._paint_texture = OpenGL.getInstance().createTexture(self._tex_width, self._tex_height) self._paint_shader.setTexture(0, self._paint_texture) def setUvPixel(self, x, y, color) -> None: self._paint_texture.setPixel(x, y, color) + def getUvTexDimensions(self): + return self._tex_width, self._tex_height + def beginRendering(self) -> None: renderer = self.getRenderer() self._checkSetup() From 33b5918acd65e2c14db5dd86893d3247b9932a5a Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Thu, 22 May 2025 11:02:08 +0200 Subject: [PATCH 013/159] Painting: Sort-of able to drag the mouse now, not just click. Also typing. The way it now works is way too slow though, and it doesn't add 'inbetween' the moude-move-positions yet. Also several other things of course. part of CURA-12543 --- plugins/PaintTool/PaintTool.py | 121 +++++++++++++++++++++------------ 1 file changed, 77 insertions(+), 44 deletions(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index 5bd0c2187d..bbcd80cef0 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -5,13 +5,15 @@ from typing import cast, Optional import numpy from PyQt6.QtCore import Qt -from typing import List, Tuple +from typing import Dict, List, Tuple from UM.Application import Application from UM.Event import Event, MouseEvent, KeyEvent 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.View import View from cura.PickingPass import PickingPass @@ -22,22 +24,27 @@ class PaintTool(Tool): def __init__(self) -> None: super().__init__() - self._shortcut_key = Qt.Key.Key_P + self._picking_pass: Optional[PickingPass] = None - self._node_cache = None + self._shortcut_key: Qt.Key = Qt.Key.Key_P + + self._node_cache: Optional[SceneNode] = None self._mesh_transformed_cache = None - self._cache_dirty = True + self._cache_dirty: bool = True - self._color_str_to_rgba = { + self._color_str_to_rgba: Dict[str, List[int]] = { "A": [192, 0, 192, 255], "B": [232, 128, 0, 255], "C": [0, 255, 0, 255], "D": [255, 255, 255, 255], } - self._brush_size = 10 - self._brush_color = "A" - self._brush_shape = "A" + self._brush_size: int = 10 + self._brush_color: str = "A" + self._brush_shape: str = "A" + + self._mouse_held: bool = False + self._mouse_drags: List[Tuple[int, int]] = [] def setPaintType(self, paint_type: str) -> None: Logger.warning(f"TODO: Implement paint-types ({paint_type}).") @@ -54,7 +61,7 @@ class PaintTool(Tool): self._brush_shape = brush_shape @staticmethod - def _get_intersect_ratio_via_pt(a, pt, b, c) -> float: + 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 # compute unit vectors of directions of lines A and B @@ -90,9 +97,41 @@ class PaintTool(Tool): case "B": include = x * x + y * y <= self._brush_size * self._brush_size if include: - res.append((mid_x + (x / w), mid_y + (y / h))) + xx = mid_x + (x / w) + yy = mid_y + (y / h) + if xx < 0 or xx > 1 or yy < 0 or yy > 1: + continue + res.append((xx, yy)) return res + def _handleMouseAction(self, node: SceneNode, paintview: View, x: int, y: int) -> bool: + face_id = self._selection_pass.getFaceIdAtPosition(x, y) + if face_id < 0: + return False + + pt = self._picking_pass.getPickedPosition(x, y).getData() + + va, vb, vc = self._mesh_transformed_cache.getFaceNodes(face_id) + ta, tb, tc = node.getMeshData().getFaceUvCoords(face_id) + + # 'Weight' of each vertex that would produce point pt, so we can generate the texture coordinates from the uv ones of the vertices. + # See (also) https://mathworld.wolfram.com/BarycentricCoordinates.html + wa = PaintTool._get_intersect_ratio_via_pt(va, pt, vb, vc) + wb = PaintTool._get_intersect_ratio_via_pt(vb, pt, vc, va) + wc = PaintTool._get_intersect_ratio_via_pt(vc, pt, va, vb) + wt = wa + wb + wc + wa /= wt + wb /= wt + wc /= wt + texcoords = wa * ta + wb * tb + wc * tc + + color = self._color_str_to_rgba[self._brush_color] + w, h = paintview.getUvTexDimensions() + for (x, y) in self._getBrushPixels(texcoords[0], texcoords[1], float(w), float(h)): + paintview.setUvPixel(x, y, color) + + return True + def event(self, event: Event) -> bool: """Handle mouse and keyboard events. @@ -118,9 +157,18 @@ class PaintTool(Tool): if event.type == Event.KeyPressEvent and cast(KeyEvent, event).key == KeyEvent.ShiftKey: return False - if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(): + if event.type == Event.MouseReleaseEvent and self._controller.getToolsEnabled(): if MouseEvent.LeftButton not in cast(MouseEvent, event).buttons: return False + + self._mouse_held = False + drags = self._mouse_drags.copy() + self._mouse_drags.clear() + + paintview = controller.getActiveView() + if paintview is None or paintview.getPluginId() != "PaintTool": + return False + if not self._selection_pass: return False @@ -144,45 +192,30 @@ class PaintTool(Tool): return False evt = cast(MouseEvent, event) + drags.append((evt.x, evt.y)) + + if not self._picking_pass: + self._picking_pass = PickingPass(camera.getViewportWidth(), camera.getViewportHeight()) + self._picking_pass.render() self._selection_pass.renderFacesMode() - face_id = self._selection_pass.getFaceIdAtPosition(evt.x, evt.y) - if face_id < 0: + + res = False + for (x, y) in drags: + res |= self._handleMouseAction(node, paintview, x, y) + return res + + if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(): + if MouseEvent.LeftButton not in cast(MouseEvent, event).buttons: return False - - ppass = PickingPass(camera.getViewportWidth(), camera.getViewportHeight()) - ppass.render() - pt = ppass.getPickedPosition(evt.x, evt.y).getData() - - va, vb, vc = self._mesh_transformed_cache.getFaceNodes(face_id) - ta, tb, tc = node.getMeshData().getFaceUvCoords(face_id) - - # 'Weight' of each vertex that would produce point pt, so we can generate the texture coordinates from the uv ones of the vertices. - # See (also) https://mathworld.wolfram.com/BarycentricCoordinates.html - wa = PaintTool._get_intersect_ratio_via_pt(va, pt, vb, vc) - wb = PaintTool._get_intersect_ratio_via_pt(vb, pt, vc, va) - wc = PaintTool._get_intersect_ratio_via_pt(vc, pt, va, vb) - wt = wa + wb + wc - wa /= wt - wb /= wt - wc /= wt - texcoords = wa * ta + wb * tb + wc * tc - - paintview = controller.getActiveView() - if paintview is None or paintview.getPluginId() != "PaintTool": - return False - color = self._color_str_to_rgba[self._brush_color] - w, h = paintview.getUvTexDimensions() - for (x, y) in self._getBrushPixels(texcoords[0], texcoords[1], float(w), float(h)): - paintview.setUvPixel(x, y, color) - + self._mouse_held = True return True if event.type == Event.MouseMoveEvent: + if not self._mouse_held: + return False evt = cast(MouseEvent, event) - return False #True - - if event.type == Event.MouseReleaseEvent: - return False #True + self._mouse_drags.append((evt.x, evt.y)) + return True return False From 86777ac666dd75bfa9a9ade7a3cdf671f4ec72ed Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 26 May 2025 11:52:53 +0200 Subject: [PATCH 014/159] Add new travel types and display z-hops CURA-11978 --- cura/LayerDataBuilder.py | 10 +- cura/LayerPolygon.py | 26 ++-- plugins/CuraEngineBackend/Cura.proto | 4 +- plugins/GCodeReader/FlavorParser.py | 17 ++- plugins/SimulationView/SimulationPass.py | 4 +- plugins/SimulationView/SimulationView.py | 6 +- plugins/SimulationView/layers.shader | 14 +- plugins/SimulationView/layers3d.shader | 67 +++++++--- plugins/SimulationView/layers3d_shadow.shader | 125 +++++++++++------- plugins/SimulationView/layers_shadow.shader | 10 +- resources/themes/cura-light/theme.json | 2 + 11 files changed, 181 insertions(+), 104 deletions(-) diff --git a/cura/LayerDataBuilder.py b/cura/LayerDataBuilder.py index d8801c9e7b..ff80307223 100755 --- a/cura/LayerDataBuilder.py +++ b/cura/LayerDataBuilder.py @@ -80,9 +80,13 @@ class LayerDataBuilder(MeshBuilder): material_colors = numpy.zeros((line_dimensions.shape[0], 4), dtype=numpy.float32) for extruder_nr in range(material_color_map.shape[0]): material_colors[extruders == extruder_nr] = material_color_map[extruder_nr] - # Set material_colors with indices where line_types (also numpy array) == MoveCombingType - material_colors[line_types == LayerPolygon.MoveCombingType] = colors[line_types == LayerPolygon.MoveCombingType] - material_colors[line_types == LayerPolygon.MoveRetractionType] = colors[line_types == LayerPolygon.MoveRetractionType] + # Set material_colors with indices where line_types (also numpy array) == MoveUnretractedType + material_colors[line_types == LayerPolygon.MoveUnretractedType] = colors[line_types == LayerPolygon.MoveUnretractedType] + material_colors[line_types == LayerPolygon.MoveRetractedType] = colors[line_types == LayerPolygon.MoveRetractedType] + material_colors[line_types == LayerPolygon.MoveWhileRetractingType] = colors[ + line_types == LayerPolygon.MoveWhileRetractingType] + material_colors[line_types == LayerPolygon.MoveWhileUnretractingType] = colors[ + line_types == LayerPolygon.MoveWhileUnretractingType] attributes = { "line_dimensions": { diff --git a/cura/LayerPolygon.py b/cura/LayerPolygon.py index e772a8b78e..c4d57c07a0 100644 --- a/cura/LayerPolygon.py +++ b/cura/LayerPolygon.py @@ -19,15 +19,21 @@ class LayerPolygon: SkirtType = 5 InfillType = 6 SupportInfillType = 7 - MoveCombingType = 8 - MoveRetractionType = 9 + MoveUnretractedType = 8 + MoveRetractedType = 9 SupportInterfaceType = 10 PrimeTowerType = 11 - __number_of_types = 12 + MoveWhileRetractingType = 12 + MoveWhileUnretractingType = 13 + __number_of_types = 14 - __jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, - numpy.arange(__number_of_types) == MoveCombingType), - numpy.arange(__number_of_types) == MoveRetractionType) + __jump_map = numpy.logical_or(numpy.logical_or(numpy.logical_or( + numpy.arange(__number_of_types) == NoneType, + numpy.arange(__number_of_types) == MoveUnretractedType), + numpy.logical_or( + numpy.arange(__number_of_types) == MoveRetractedType, + numpy.arange(__number_of_types) == MoveWhileRetractingType)), + numpy.arange(__number_of_types) == MoveWhileUnretractingType) def __init__(self, extruder: int, line_types: numpy.ndarray, data: numpy.ndarray, line_widths: numpy.ndarray, line_thicknesses: numpy.ndarray, line_feedrates: numpy.ndarray) -> None: @@ -269,10 +275,12 @@ class LayerPolygon: theme.getColor("layerview_skirt").getRgbF(), # SkirtType theme.getColor("layerview_infill").getRgbF(), # InfillType theme.getColor("layerview_support_infill").getRgbF(), # SupportInfillType - theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType - theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractionType + theme.getColor("layerview_move_combing").getRgbF(), # MoveUnretractedType + theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractedType theme.getColor("layerview_support_interface").getRgbF(), # SupportInterfaceType - theme.getColor("layerview_prime_tower").getRgbF() # PrimeTowerType + theme.getColor("layerview_prime_tower").getRgbF(), # PrimeTowerType + theme.getColor("layerview_move_while_retracting").getRgbF(), # MoveWhileRetracting + theme.getColor("layerview_move_while_unretracting").getRgbF(), # MoveWhileUnretracting ]) return cls.__color_map diff --git a/plugins/CuraEngineBackend/Cura.proto b/plugins/CuraEngineBackend/Cura.proto index 238829ba64..8018c9186f 100644 --- a/plugins/CuraEngineBackend/Cura.proto +++ b/plugins/CuraEngineBackend/Cura.proto @@ -78,8 +78,8 @@ message Polygon { SkirtType = 5; InfillType = 6; SupportInfillType = 7; - MoveCombingType = 8; - MoveRetractionType = 9; + MoveUnretractedType = 8; + MoveRetractedType = 9; SupportInterfaceType = 10; PrimeTowerType = 11; } diff --git a/plugins/GCodeReader/FlavorParser.py b/plugins/GCodeReader/FlavorParser.py index 74dbeadec0..f83a9bbb34 100644 --- a/plugins/GCodeReader/FlavorParser.py +++ b/plugins/GCodeReader/FlavorParser.py @@ -133,7 +133,10 @@ class FlavorParser: if i > 0: line_feedrates[i - 1] = point[3] line_types[i - 1] = point[5] - if point[5] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]: + if point[5] in [LayerPolygon.MoveUnretractedType, + LayerPolygon.MoveRetractedType, + LayerPolygon.MoveWhileRetractingType, + LayerPolygon.MoveWhileUnretractingType]: line_widths[i - 1] = 0.1 line_thicknesses[i - 1] = 0.0 # Travels are set as zero thickness lines else: @@ -196,7 +199,7 @@ class FlavorParser: path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], self._layer_type]) # extrusion self._previous_extrusion_value = new_extrusion_value else: - path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) # retraction + path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractedType]) # retraction e[self._extruder_number] = new_extrusion_value # Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions @@ -205,9 +208,9 @@ class FlavorParser: self._current_layer_thickness = z - self._previous_z # allow a tiny overlap self._previous_z = z elif self._previous_extrusion_value > e[self._extruder_number]: - path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) + path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractedType]) else: - path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType]) + path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveUnretractedType]) return self._position(x, y, z, f, e) @@ -419,7 +422,7 @@ class FlavorParser: self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])) current_path.clear() # Start the new layer at the end position of the last layer - current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType]) + current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveUnretractedType]) # When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior # as in ProcessSlicedLayersJob @@ -461,9 +464,9 @@ class FlavorParser: # When changing tool, store the end point of the previous path, then process the code and finally # add another point with the new position of the head. - current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType]) + current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveUnretractedType]) current_position = self.processTCode(global_stack, T, line, current_position, current_path) - current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType]) + current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveUnretractedType]) if line.startswith("M"): M = self._getInt(line, "M") diff --git a/plugins/SimulationView/SimulationPass.py b/plugins/SimulationView/SimulationPass.py index 080b02bd9e..840fd7f6dc 100644 --- a/plugins/SimulationView/SimulationPass.py +++ b/plugins/SimulationView/SimulationPass.py @@ -202,9 +202,9 @@ class SimulationPass(RenderPass): self._layer_shader.setUniformValue("u_next_vertex", not_a_vector) self._layer_shader.setUniformValue("u_last_line_ratio", 1.0) - # The first line does not have a previous line: add a MoveCombingType in front for start detection + # The first line does not have a previous line: add a MoveUnretractedType in front for start detection # this way the first start of the layer can also be drawn - prev_line_types = numpy.concatenate([numpy.asarray([LayerPolygon.MoveCombingType], dtype = numpy.float32), layer_data._attributes["line_types"]["value"]]) + prev_line_types = numpy.concatenate([numpy.asarray([LayerPolygon.MoveUnretractedType], dtype = numpy.float32), layer_data._attributes["line_types"]["value"]]) # Remove the last element prev_line_types = prev_line_types[0:layer_data._attributes["line_types"]["value"].size] layer_data._attributes["prev_line_types"] = {'opengl_type': 'float', 'value': prev_line_types, 'opengl_name': 'a_prev_line_type'} diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index 10861acfd0..083fc73bf1 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -608,8 +608,10 @@ class SimulationView(CuraView): visible_line_types.append(LayerPolygon.SupportInterfaceType) visible_line_types_with_extrusion = visible_line_types.copy() # Copy before travel moves are added if self.getShowTravelMoves(): - visible_line_types.append(LayerPolygon.MoveCombingType) - visible_line_types.append(LayerPolygon.MoveRetractionType) + visible_line_types.append(LayerPolygon.MoveUnretractedType) + visible_line_types.append(LayerPolygon.MoveRetractedType) + visible_line_types.append(LayerPolygon.MoveWhileRetractingType) + visible_line_types.append(LayerPolygon.MoveWhileUnretractingType) for node in DepthFirstIterator(self.getController().getScene().getRoot()): layer_data = node.callDecoration("getLayerData") diff --git a/plugins/SimulationView/layers.shader b/plugins/SimulationView/layers.shader index e6210c2b65..d5079fd82b 100644 --- a/plugins/SimulationView/layers.shader +++ b/plugins/SimulationView/layers.shader @@ -22,8 +22,8 @@ vertex = gl_Position = u_projectionMatrix * u_viewMatrix * u_modelMatrix * a_vertex; // shade the color depending on the extruder index v_color = a_color; - // 8 and 9 are travel moves - if ((a_line_type != 8.0) && (a_line_type != 9.0)) { + // 8, 9, 12 and 13 are travel moves + if ((a_line_type != 8.0) && (a_line_type != 9.0) && (a_line_type != 12.0) && (a_line_type != 13.0)) { v_color = (a_extruder == u_active_extruder) ? v_color : vec4(u_shade_factor * v_color.rgb, v_color.a); } @@ -48,7 +48,9 @@ fragment = void main() { - if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5)) { // actually, 8 and 9 + // travel moves: 8, 9, 12, 13 + if ((u_show_travel_moves == 0) && (((v_line_type >= 7.5) && (v_line_type <= 9.5)) || + ((v_line_type >= 11.5) && (v_line_type <= 13.5)))) { // discard movements discard; } @@ -100,7 +102,7 @@ vertex41core = { gl_Position = u_projectionMatrix * u_viewMatrix * u_modelMatrix * a_vertex; v_color = a_color; - if ((a_line_type != 8) && (a_line_type != 9)) { + if ((a_line_type != 8) && (a_line_type != 9) && (a_line_type != 12) && (a_line_type != 13)) { v_color = (a_extruder == u_active_extruder) ? v_color : vec4(u_shade_factor * v_color.rgb, v_color.a); } @@ -120,7 +122,9 @@ fragment41core = void main() { - if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5)) { // actually, 8 and 9 + // travel moves: 8, 9, 12, 13 + if ((u_show_travel_moves == 0) && (((v_line_type >= 7.5) && (v_line_type <= 9.5)) || + ((v_line_type >= 11.5) && (v_line_type <= 13.5)))) { // discard movements discard; } diff --git a/plugins/SimulationView/layers3d.shader b/plugins/SimulationView/layers3d.shader index 494a07083d..e2f57823f3 100644 --- a/plugins/SimulationView/layers3d.shader +++ b/plugins/SimulationView/layers3d.shader @@ -228,22 +228,26 @@ geometry41core = { highp mat4 viewProjectionMatrix = u_projectionMatrix * u_viewMatrix; - vec4 g_vertex_delta; - vec3 g_vertex_normal_horz; // horizontal and vertical in respect to layers - vec4 g_vertex_offset_horz; // vec4 to match gl_in[x].gl_Position + // Vertices are declared as vec4 so that they can be used for calculations with gl_in[x].gl_Position + vec3 g_vertex_delta; + vec3 g_vertex_normal_horz; + vec4 g_vertex_offset_horz; vec3 g_vertex_normal_vert; vec4 g_vertex_offset_vert; vec3 g_vertex_normal_horz_head; vec4 g_vertex_offset_horz_head; + vec3 g_axial_plan_vector; + vec3 g_radial_plan_vector; float size_x; float size_y; - if ((v_extruder_opacity[0][int(mod(v_extruder[0], 4))][v_extruder[0] / 4] == 0.0) && (v_line_type[0] != 8) && (v_line_type[0] != 9)) { + if ((v_extruder_opacity[0][int(mod(v_extruder[0], 4))][v_extruder[0] / 4] == 0.0) && + (v_line_type[0] != 8) && (v_line_type[0] != 9) && (v_line_type[0] != 12) && (v_line_type[0] != 13)) { return; } - // See LayerPolygon; 8 is MoveCombingType, 9 is RetractionType - if ((u_show_travel_moves == 0) && ((v_line_type[0] == 8) || (v_line_type[0] == 9))) { + // See LayerPolygon; 8 is MoveUnretractedType, 9 is RetractionType, 12 is MoveWhileRetractingType, 13 is MoveWhileUnretractingType + if ((u_show_travel_moves == 0) && ((v_line_type[0] == 8) || (v_line_type[0] == 9) || (v_line_type[0] == 12) || (v_line_type[0] == 13))) { return; } if ((u_show_helpers == 0) && ((v_line_type[0] == 4) || (v_line_type[0] == 5) || (v_line_type[0] == 7) || (v_line_type[0] == 10) || v_line_type[0] == 11)) { @@ -256,7 +260,7 @@ geometry41core = return; } - if ((v_line_type[0] == 8) || (v_line_type[0] == 9)) { + if ((v_line_type[0] == 8) || (v_line_type[0] == 9) || (v_line_type[0] == 12) || (v_line_type[0] == 13)) { // fixed size for movements size_x = 0.05; } else { @@ -264,26 +268,47 @@ geometry41core = } size_y = v_line_dim[1].y / 2 + 0.01; - g_vertex_delta = gl_in[1].gl_Position - gl_in[0].gl_Position; //Actual movement exhibited by the line. - g_vertex_normal_horz_head = normalize(vec3(-g_vertex_delta.x, -g_vertex_delta.y, -g_vertex_delta.z)); //Lengthwise normal vector pointing backwards. - g_vertex_offset_horz_head = vec4(g_vertex_normal_horz_head * size_x, 0.0); //Lengthwise offset vector pointing backwards. + g_vertex_delta = (gl_in[1].gl_Position - gl_in[0].gl_Position).xyz; //Actual movement exhibited by the line. - g_vertex_normal_horz = normalize(vec3(g_vertex_delta.z, g_vertex_delta.y, -g_vertex_delta.x)); //Normal vector pointing right. + if (g_vertex_delta == vec3(0.0)) { + return; + } + + if (g_vertex_delta.y == 0.0) + { + // vector is in the horizontal plan, radial vector is a simple rotation around Y axis + g_radial_plan_vector = vec3(g_vertex_delta.z, 0.0, -g_vertex_delta.x); + } + else if(g_vertex_delta.x == 0.0 && g_vertex_delta.z == 0.0) + { + // delta vector is purely vertical, display the line rotated vertically so that it is visible in front and side views + g_radial_plan_vector = vec3(1.0, 0.0, -1.0); + } + else + { + // delta vector is completely 3D + g_axial_plan_vector = vec3(g_vertex_delta.x, 0.0, g_vertex_delta.z); // Vector projected in the horizontal plan + g_radial_plan_vector = cross(g_vertex_delta, g_axial_plan_vector); // Radial vector in the horizontal plan, pointing right. + } + + g_vertex_normal_horz_head = normalize(g_vertex_delta); //Lengthwise normal vector + g_vertex_offset_horz_head = vec4(g_vertex_normal_horz_head * size_x, 0.0); //Lengthwise offset vector + + g_vertex_normal_horz = normalize(g_radial_plan_vector); //Normal vector pointing right. g_vertex_offset_horz = vec4(g_vertex_normal_horz * size_x, 0.0); //Offset vector pointing right. g_vertex_normal_vert = vec3(0.0, 1.0, 0.0); //Upwards normal vector. g_vertex_offset_vert = vec4(g_vertex_normal_vert * size_y, 0.0); //Upwards offset vector. Goes up by half the layer thickness. - if ((v_line_type[0] == 8) || (v_line_type[0] == 9)) { //Travel or retraction moves. - vec4 va_head = viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head + g_vertex_offset_vert); + if ((v_line_type[0] == 8) || (v_line_type[0] == 9) || (v_line_type[0] == 12) || (v_line_type[0] == 13)) { //Travel or retraction moves. + vec4 va_head = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz_head + g_vertex_offset_vert); vec4 va_up = viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert); vec4 va_down = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert); - vec4 vb_head = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head + g_vertex_offset_vert); + vec4 vb_head = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz_head + g_vertex_offset_vert); vec4 vb_down = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert); vec4 vb_up = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert); // Travels: flat plane with pointy ends - myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_up); myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_head); myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_down); myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_up); @@ -308,8 +333,8 @@ geometry41core = vec4 vb_p_horz = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz); //Line end, right vertex. vec4 va_m_vert = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert); //Line start, bottom vertex. vec4 vb_m_vert = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert); //Line end, bottom vertex. - vec4 va_head = viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head); //Line start, tip. - vec4 vb_head = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head); //Line end, tip. + vec4 va_head = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz_head); //Line start, tip. + vec4 vb_head = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz_head); //Line end, tip. // All normal lines are rendered as 3d tubes. myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz, va_m_horz); @@ -328,14 +353,14 @@ geometry41core = // left side myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz, va_m_horz); myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_p_vert); - myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_horz_head, va_head); + myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz_head, va_head); myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_horz, va_p_horz); EndPrimitive(); myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_horz, va_p_horz); myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_vert, va_m_vert); - myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_horz_head, va_head); + myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz_head, va_head); myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz, va_m_horz); EndPrimitive(); @@ -343,14 +368,14 @@ geometry41core = // right side myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, vb_p_horz); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, vb_p_vert); - myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz_head, vb_head); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz_head, vb_head); myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, vb_m_horz); EndPrimitive(); myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, vb_m_horz); myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, vb_m_vert); - myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz_head, vb_head); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz_head, vb_head); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, vb_p_horz); EndPrimitive(); diff --git a/plugins/SimulationView/layers3d_shadow.shader b/plugins/SimulationView/layers3d_shadow.shader index 88268938c9..47637cfb20 100644 --- a/plugins/SimulationView/layers3d_shadow.shader +++ b/plugins/SimulationView/layers3d_shadow.shader @@ -95,22 +95,26 @@ geometry41core = { highp mat4 viewProjectionMatrix = u_projectionMatrix * u_viewMatrix; - vec4 g_vertex_delta; - vec3 g_vertex_normal_horz; // horizontal and vertical in respect to layers - vec4 g_vertex_offset_horz; // vec4 to match gl_in[x].gl_Position + // Vertices are declared as vec4 so that they can be used for calculations with gl_in[x].gl_Position + vec3 g_vertex_delta; + vec3 g_vertex_normal_horz; + vec4 g_vertex_offset_horz; vec3 g_vertex_normal_vert; vec4 g_vertex_offset_vert; vec3 g_vertex_normal_horz_head; vec4 g_vertex_offset_horz_head; + vec3 g_axial_plan_vector; + vec3 g_radial_plan_vector; float size_x; float size_y; - if ((v_extruder_opacity[0][int(mod(v_extruder[0], 4))][v_extruder[0] / 4] == 0.0) && (v_line_type[0] != 8) && (v_line_type[0] != 9)) { + if ((v_extruder_opacity[0][int(mod(v_extruder[0], 4))][v_extruder[0] / 4] == 0.0) && + (v_line_type[0] != 8) && (v_line_type[0] != 9) && (v_line_type[0] != 12) && (v_line_type[0] != 13)) { return; } - // See LayerPolygon; 8 is MoveCombingType, 9 is RetractionType - if ((u_show_travel_moves == 0) && ((v_line_type[0] == 8) || (v_line_type[0] == 9))) { + // See LayerPolygon; 8 is MoveUnretractedType, 9 is RetractionType, 12 is MoveWhileRetractingType, 13 is MoveWhileUnretractingType + if ((u_show_travel_moves == 0) && ((v_line_type[0] == 8) || (v_line_type[0] == 9) || (v_line_type[0] == 12) || (v_line_type[0] == 13))) { return; } if ((u_show_helpers == 0) && ((v_line_type[0] == 4) || (v_line_type[0] == 5) || (v_line_type[0] == 7) || (v_line_type[0] == 10))) { @@ -123,7 +127,7 @@ geometry41core = return; } - if ((v_line_type[0] == 8) || (v_line_type[0] == 9)) { + if ((v_line_type[0] == 8) || (v_line_type[0] == 9) || (v_line_type[0] == 12) || (v_line_type[0] == 13)) { // fixed size for movements size_x = 0.05; } else { @@ -131,93 +135,114 @@ geometry41core = } size_y = v_line_dim[1].y / 2 + 0.01; - g_vertex_delta = gl_in[1].gl_Position - gl_in[0].gl_Position; - g_vertex_normal_horz_head = normalize(vec3(-g_vertex_delta.x, -g_vertex_delta.y, -g_vertex_delta.z)); - g_vertex_offset_horz_head = vec4(g_vertex_normal_horz_head * size_x, 0.0); + g_vertex_delta = (gl_in[1].gl_Position - gl_in[0].gl_Position).xyz; //Actual movement exhibited by the line. - g_vertex_normal_horz = normalize(vec3(g_vertex_delta.z, g_vertex_delta.y, -g_vertex_delta.x)); + if (g_vertex_delta == vec3(0.0)) { + return; + } - g_vertex_offset_horz = vec4(g_vertex_normal_horz * size_x, 0.0); //size * g_vertex_normal_horz; - g_vertex_normal_vert = vec3(0.0, 1.0, 0.0); - g_vertex_offset_vert = vec4(g_vertex_normal_vert * size_y, 0.0); + if (g_vertex_delta.y == 0.0) + { + // vector is in the horizontal plan, radial vector is a simple rotation around Y axis + g_radial_plan_vector = vec3(g_vertex_delta.z, 0.0, -g_vertex_delta.x); + } + else if(g_vertex_delta.x == 0.0 && g_vertex_delta.z == 0.0) + { + // delta vector is purely vertical, display the line rotated vertically so that it is visible in front and side views + g_radial_plan_vector = vec3(1.0, 0.0, -1.0); + } + else + { + // delta vector is completely 3D + g_axial_plan_vector = vec3(g_vertex_delta.x, 0.0, g_vertex_delta.z); // Vector projected in the horizontal plan + g_radial_plan_vector = cross(g_vertex_delta, g_axial_plan_vector); // Radial vector in the horizontal plan, pointing right. + } - if ((v_line_type[0] == 8) || (v_line_type[0] == 9)) { - vec4 va_head = viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head + g_vertex_offset_vert); + g_vertex_normal_horz_head = normalize(g_vertex_delta); //Lengthwise normal vector + g_vertex_offset_horz_head = vec4(g_vertex_normal_horz_head * size_x, 0.0); //Lengthwise offset vector + + g_vertex_normal_horz = normalize(g_radial_plan_vector); //Normal vector pointing right. + g_vertex_offset_horz = vec4(g_vertex_normal_horz * size_x, 0.0); //Offset vector pointing right. + + g_vertex_normal_vert = vec3(0.0, 1.0, 0.0); //Upwards normal vector. + g_vertex_offset_vert = vec4(g_vertex_normal_vert * size_y, 0.0); //Upwards offset vector. Goes up by half the layer thickness. + + if ((v_line_type[0] == 8) || (v_line_type[0] == 9) || (v_line_type[0] == 12) || (v_line_type[0] == 13)) { + vec4 va_head = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz_head + g_vertex_offset_vert); vec4 va_up = viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert); vec4 va_down = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert); - vec4 vb_head = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head + g_vertex_offset_vert); + vec4 vb_head = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz_head + g_vertex_offset_vert); vec4 vb_down = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert); vec4 vb_up = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert); // Travels: flat plane with pointy ends - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, va_up); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, va_head); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, va_down); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, va_up); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_head); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_down); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_up); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, vb_down); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, vb_up); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, vb_head); //And reverse so that the line is also visible from the back side. myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, vb_up); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, vb_down); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, va_up); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, va_down); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, va_head); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, va_up); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_up); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_down); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_head); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_up); EndPrimitive(); } else { - vec4 va_m_horz = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz); - vec4 vb_m_horz = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz); - vec4 va_p_vert = viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert); - vec4 vb_p_vert = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert); - vec4 va_p_horz = viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz); - vec4 vb_p_horz = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz); - vec4 va_m_vert = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert); - vec4 vb_m_vert = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert); - vec4 va_head = viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head); - vec4 vb_head = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head); + vec4 va_m_horz = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz); //Line start, left vertex. + vec4 vb_m_horz = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz); //Line end, left vertex. + vec4 va_p_vert = viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert); //Line start, top vertex. + vec4 vb_p_vert = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert); //Line end, top vertex. + vec4 va_p_horz = viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz); //Line start, right vertex. + vec4 vb_p_horz = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz); //Line end, right vertex. + vec4 va_m_vert = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert); //Line start, bottom vertex. + vec4 vb_m_vert = viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert); //Line end, bottom vertex. + vec4 va_head = viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz_head); //Line start, tip. + vec4 vb_head = viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz_head); //Line end, tip. // All normal lines are rendered as 3d tubes. - myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, va_m_horz); + myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz, va_m_horz); myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, vb_m_horz); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, va_p_vert); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_p_vert); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, vb_p_vert); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, va_p_horz); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_horz, va_p_horz); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, vb_p_horz); - myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, va_m_vert); + myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_vert, va_m_vert); myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, vb_m_vert); - myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, va_m_horz); + myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz, va_m_horz); myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, vb_m_horz); EndPrimitive(); // left side - myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, va_m_horz); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, va_p_vert); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, va_head); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, va_p_horz); + myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz, va_m_horz); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_vert, va_p_vert); + myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz_head, va_head); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_horz, va_p_horz); EndPrimitive(); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, va_p_horz); - myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, va_m_vert); - myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, va_head); - myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, va_m_horz); + myEmitVertex(v_vertex[0], v_color[1], g_vertex_normal_horz, va_p_horz); + myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_vert, va_m_vert); + myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz_head, va_head); + myEmitVertex(v_vertex[0], v_color[1], -g_vertex_normal_horz, va_m_horz); EndPrimitive(); // right side myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, vb_p_horz); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, vb_p_vert); - myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz_head, vb_head); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz_head, vb_head); myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, vb_m_horz); EndPrimitive(); myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, vb_m_horz); myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, vb_m_vert); - myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz_head, vb_head); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz_head, vb_head); myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, vb_p_horz); EndPrimitive(); diff --git a/plugins/SimulationView/layers_shadow.shader b/plugins/SimulationView/layers_shadow.shader index 4bc2de3d0b..73278914b7 100644 --- a/plugins/SimulationView/layers_shadow.shader +++ b/plugins/SimulationView/layers_shadow.shader @@ -48,8 +48,10 @@ fragment = void main() { - if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5)) - { // actually, 8 and 9 + // travel moves: 8, 9, 12, 13 + if ((u_show_travel_moves == 0) && (((v_line_type >= 7.5) && (v_line_type <= 9.5)) || + ((v_line_type >= 11.5) && (v_line_type <= 13.5)))) { + { // discard movements discard; } @@ -124,7 +126,9 @@ fragment41core = void main() { - if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5)) { // actually, 8 and 9 + // travel moves: 8, 9, 12, 13 + if ((u_show_travel_moves == 0) && (((v_line_type >= 7.5) && (v_line_type <= 9.5)) || + ((v_line_type >= 11.5) && (v_line_type <= 13.5)))) { // discard movements discard; } diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 1ae316f96c..377e70f5b6 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -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], From e1d579c6c8f7720deb05dd21e3892b1f914c6e34 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 26 May 2025 16:53:34 +0200 Subject: [PATCH 015/159] Display legend tooltip for travel types CURA-11978 --- .../SimulationViewMenuComponent.qml | 87 +++++++++++++++++-- 1 file changed, 82 insertions(+), 5 deletions(-) diff --git a/plugins/SimulationView/SimulationViewMenuComponent.qml b/plugins/SimulationView/SimulationViewMenuComponent.qml index d434d883eb..dcd2c7d178 100644 --- a/plugins/SimulationView/SimulationViewMenuComponent.qml +++ b/plugins/SimulationView/SimulationViewMenuComponent.qml @@ -227,29 +227,52 @@ Cura.ExpandableComponent id: typesLegendModel Component.onCompleted: { + const travelsTypesModel = [ + { + label: catalog.i18nc("@label", "Unretracted"), + colorId: "layerview_move_combing" + }, + { + label: catalog.i18nc("@label", "Retracted"), + colorId: "layerview_move_retraction" + }, + { + label: catalog.i18nc("@label", "Retracting"), + colorId: "layerview_move_while_retracting" + }, + { + label: catalog.i18nc("@label", "Unretracting"), + colorId: "layerview_move_while_unretracting" + } + ]; + typesLegendModel.append({ label: catalog.i18nc("@label", "Travels"), initialValue: viewSettings.show_travel_moves, preference: "layerview/show_travel_moves", - colorId: "layerview_move_combing" + colorId: "layerview_move_combing", + subTypesModel: travelsTypesModel }); typesLegendModel.append({ label: catalog.i18nc("@label", "Helpers"), initialValue: viewSettings.show_helpers, preference: "layerview/show_helpers", - colorId: "layerview_support" + colorId: "layerview_support", + subTypesModel: [] }); typesLegendModel.append({ label: catalog.i18nc("@label", "Shell"), initialValue: viewSettings.show_skin, preference: "layerview/show_skin", - colorId: "layerview_inset_0" + colorId: "layerview_inset_0", + subTypesModel: [] }); typesLegendModel.append({ label: catalog.i18nc("@label", "Infill"), initialValue: viewSettings.show_infill, preference: "layerview/show_infill", - colorId: "layerview_infill" + colorId: "layerview_infill", + subTypesModel: [] }); if (! UM.SimulationView.compatibilityMode) { @@ -257,7 +280,8 @@ Cura.ExpandableComponent label: catalog.i18nc("@label", "Starts"), initialValue: viewSettings.show_starts, preference: "layerview/show_starts", - colorId: "layerview_starts" + colorId: "layerview_starts", + subTypesModel: [] }); } } @@ -273,6 +297,7 @@ Cura.ExpandableComponent Rectangle { + id: rectangleColor anchors.verticalCenter: parent.verticalCenter anchors.right: legendModelCheckBox.right width: UM.Theme.getSize("layerview_legend_size").width @@ -281,6 +306,58 @@ Cura.ExpandableComponent border.width: UM.Theme.getSize("default_lining").width border.color: UM.Theme.getColor("lining") visible: viewSettings.show_legend + + MouseArea + { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + enabled: subTypesModel.count > 0 + + onEntered: tooltip.show() + onExited: tooltip.hide() + + UM.ToolTip + { + id: tooltip + delay: 0 + width: subTypesColumn.implicitWidth + 2 * UM.Theme.getSize("thin_margin").width + height: subTypesColumn.implicitHeight + 2 * UM.Theme.getSize("thin_margin").width + + contentItem: Column + { + id: subTypesColumn + padding: 0 + spacing: UM.Theme.getSize("layerview_row_spacing").height + + Repeater + { + model: subTypesModel + UM.Label + { + text: label + + height: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height + width: UM.Theme.getSize("layerview_menu_size").width + color: UM.Theme.getColor("setting_control_text") + Rectangle + { + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + + width: UM.Theme.getSize("layerview_legend_size").width + height: UM.Theme.getSize("layerview_legend_size").height + + color: UM.Theme.getColor(model.colorId) + + border.width: UM.Theme.getSize("default_lining").width + border.color: UM.Theme.getColor("lining") + } + } + } + } + } + } } UM.Label From 704f9453f0a1cfd554eb4b18cb31b293fa4c917e Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 27 May 2025 17:03:38 +0200 Subject: [PATCH 016/159] Properly completed drag to paint (no more just clicking points). The most important thing to make it work is actually notifying the scene that something has changed -- the rest are just refactorings and (hopefully) optimizations. part of CURA-12543 --- plugins/PaintTool/PaintTool.py | 48 +++++++++++++++++---------------- plugins/PaintTool/PaintTool.qml | 2 +- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index bbcd80cef0..503d380987 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -1,11 +1,10 @@ # Copyright (c) 2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. - -from typing import cast, Optional +from copy import deepcopy import numpy from PyQt6.QtCore import Qt -from typing import Dict, List, Tuple +from typing import cast, Dict, List, Optional, Tuple from UM.Application import Application from UM.Event import Event, MouseEvent, KeyEvent @@ -14,6 +13,7 @@ from UM.Scene.SceneNode import SceneNode from UM.Scene.Selection import Selection from UM.Tool import Tool from UM.View.View import View + from cura.PickingPass import PickingPass @@ -44,7 +44,7 @@ class PaintTool(Tool): self._brush_shape: str = "A" self._mouse_held: bool = False - self._mouse_drags: List[Tuple[int, int]] = [] + self._last_mouse_drag: Optional[Tuple[int, int]] = None def setPaintType(self, paint_type: str) -> None: Logger.warning(f"TODO: Implement paint-types ({paint_type}).") @@ -52,7 +52,6 @@ class PaintTool(Tool): def setBrushSize(self, brush_size: float) -> None: self._brush_size = int(brush_size) - print(self._brush_size) def setBrushColor(self, brush_color: str) -> None: self._brush_color = brush_color @@ -89,8 +88,8 @@ class PaintTool(Tool): def _getBrushPixels(self, mid_x: float, mid_y: float, w: float, h: float) -> List[Tuple[float, float]]: res = [] include = False - for y in range(-self._brush_size, self._brush_size + 1): - for x in range(-self._brush_size, self._brush_size + 1): + for y in range(-self._brush_size//2, (self._brush_size + 1)//2): + for x in range(-self._brush_size//2, (self._brush_size + 1)//2): match self._brush_shape: case "A": include = True @@ -160,10 +159,24 @@ class PaintTool(Tool): if event.type == Event.MouseReleaseEvent and self._controller.getToolsEnabled(): if MouseEvent.LeftButton not in cast(MouseEvent, event).buttons: return False - self._mouse_held = False - drags = self._mouse_drags.copy() - self._mouse_drags.clear() + self._last_mouse_drag = None + return True + + is_moved = event.type == Event.MouseMoveEvent + is_pressed = event.type == Event.MousePressEvent + if (is_moved or is_pressed) and self._controller.getToolsEnabled(): + if is_moved and not self._mouse_held: + return False + + evt = cast(MouseEvent, event) + if is_pressed: + if MouseEvent.LeftButton not in evt.buttons: + return False + else: + self._mouse_held = True + drags = ([self._last_mouse_drag] if self._last_mouse_drag else []) + [(evt.x, evt.y)] + self._last_mouse_drag = (evt.x, evt.y) paintview = controller.getActiveView() if paintview is None or paintview.getPluginId() != "PaintTool": @@ -203,19 +216,8 @@ class PaintTool(Tool): res = False for (x, y) in drags: res |= self._handleMouseAction(node, paintview, x, y) + if res: + Application.getInstance().getController().getScene().sceneChanged.emit(node) return res - if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(): - if MouseEvent.LeftButton not in cast(MouseEvent, event).buttons: - return False - self._mouse_held = True - return True - - if event.type == Event.MouseMoveEvent: - if not self._mouse_held: - return False - evt = cast(MouseEvent, event) - self._mouse_drags.append((evt.x, evt.y)) - return True - return False diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml index 902d5a700d..82d6d90ffd 100644 --- a/plugins/PaintTool/PaintTool.qml +++ b/plugins/PaintTool/PaintTool.qml @@ -164,7 +164,7 @@ Item id: shapeSizeSlider from: 1 - to: 50 + to: 40 value: 10 onPressedChanged: function(pressed) From 96d2caf19542e024b38187fabbb4ea899ec85020 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 28 May 2025 11:00:55 +0200 Subject: [PATCH 017/159] Load UV coordinates from 3MF file CURA-12544 --- plugins/3MFReader/ThreeMFReader.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index 45ab2e7d2f..5c8d803f3b 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -94,7 +94,7 @@ class ThreeMFReader(MeshReader): return temp_mat @staticmethod - def _convertSavitarNodeToUMNode(savitar_node: Savitar.SceneNode, file_name: str = "", archive: zipfile.ZipFile = None) -> Optional[SceneNode]: + def _convertSavitarNodeToUMNode(savitar_node: Savitar.SceneNode, file_name: str = "", archive: zipfile.ZipFile = None, scene: Savitar.Scene = None) -> Optional[SceneNode]: """Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node. :returns: Scene node. @@ -131,12 +131,19 @@ class ThreeMFReader(MeshReader): um_node.setTransformation(transformation) mesh_builder = MeshBuilder() - data = numpy.fromstring(savitar_node.getMeshData().getFlatVerticesAsBytes(), dtype=numpy.float32) + mesh_data = savitar_node.getMeshData() + + vertices_data = numpy.fromstring(mesh_data.getFlatVerticesAsBytes(), dtype=numpy.float32) + vertices = numpy.resize(vertices_data, (int(vertices_data.size / 3), 3)) + + texture_path = mesh_data.getTexturePath(scene) + uv_data = numpy.fromstring(mesh_data.getUVCoordinatesPerVertexAsBytes(scene), dtype=numpy.float32) + uv_coordinates = numpy.resize(uv_data, (int(uv_data.size / 2), 2)) - vertices = numpy.resize(data, (int(data.size / 3), 3)) mesh_builder.setVertices(vertices) mesh_builder.calculateNormals(fast=True) mesh_builder.setMeshId(node_id) + mesh_builder.setUVCoordinates(uv_coordinates) if file_name: # The filename is used to give the user the option to reload the file if it is changed on disk # It is only set for the root node of the 3mf file @@ -147,7 +154,7 @@ class ThreeMFReader(MeshReader): um_node.setMeshData(mesh_data) for child in savitar_node.getChildren(): - child_node = ThreeMFReader._convertSavitarNodeToUMNode(child, archive=archive) + child_node = ThreeMFReader._convertSavitarNodeToUMNode(child, archive=archive, scene=scene) if child_node: um_node.addChild(child_node) @@ -236,7 +243,7 @@ class ThreeMFReader(MeshReader): CuraApplication.getInstance().getController().getScene().setMetaDataEntry(key, value) for node in scene_3mf.getSceneNodes(): - um_node = ThreeMFReader._convertSavitarNodeToUMNode(node, file_name, archive) + um_node = ThreeMFReader._convertSavitarNodeToUMNode(node, file_name, archive, scene_3mf) if um_node is None: continue @@ -336,7 +343,7 @@ class ThreeMFReader(MeshReader): # Convert the scene to scene nodes nodes = [] for savitar_node in scene.getSceneNodes(): - scene_node = ThreeMFReader._convertSavitarNodeToUMNode(savitar_node, "file_name") + scene_node = ThreeMFReader._convertSavitarNodeToUMNode(savitar_node, "file_name", scene=scene) if scene_node is None: continue nodes.append(scene_node) From 109f37657b8417671846398de6bb39edfc7861bf Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 28 May 2025 12:32:36 +0200 Subject: [PATCH 018/159] Painting UI work: Update image-part(s) instead of pixel(s) w.r.t. render-backend. part of CURA-12543 --- plugins/PaintTool/PaintTool.py | 134 ++++++++++++++++++++++----------- plugins/PaintTool/PaintView.py | 12 ++- 2 files changed, 98 insertions(+), 48 deletions(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index 503d380987..90482d0055 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -1,9 +1,9 @@ # Copyright (c) 2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. -from copy import deepcopy import numpy from PyQt6.QtCore import Qt +from PyQt6.QtGui import QImage, QPainter, QColor, QBrush from typing import cast, Dict, List, Optional, Tuple from UM.Application import Application @@ -12,9 +12,9 @@ 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.View import View from cura.PickingPass import PickingPass +from .PaintView import PaintView class PaintTool(Tool): @@ -42,22 +42,82 @@ class PaintTool(Tool): self._brush_size: int = 10 self._brush_color: str = "A" self._brush_shape: str = "A" + self._brush_image = self._createBrushImage() self._mouse_held: bool = False - self._last_mouse_drag: Optional[Tuple[int, int]] = None + self._last_text_coords: Optional[Tuple[int, int]] = None + + def _createBrushImage(self) -> QImage: + brush_image = QImage(self._brush_size, self._brush_size, QImage.Format.Format_RGBA8888) + brush_image.fill(QColor(255,255,255,0)) + + color = self._color_str_to_rgba[self._brush_color] + qcolor = QColor(color[0], color[1], color[2], color[3]) + + painter = QPainter(brush_image) + painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) + painter.setPen(Qt.PenStyle.NoPen) + painter.setBrush(QBrush(qcolor)) + match self._brush_shape: + case "A": # Square brush + painter.drawRect(0, 0, self._brush_size, self._brush_size) + case "B": # Circle brush + painter.drawEllipse(0, 0, self._brush_size, self._brush_size) + case _: + painter.drawRect(0, 0, self._brush_size, self._brush_size) + painter.end() + + return brush_image + + def _createStrokeImage(self, x0: float, y0: float, x1: float, y1: float) -> Tuple[QImage, Tuple[int, int]]: + distance = numpy.hypot(x1 - x0, y1 - y0) + angle = numpy.arctan2(y1 - y0, x1 - x0) + stroke_width = self._brush_size + stroke_height = int(distance) + self._brush_size + + half_brush_size = self._brush_size // 2 + start_x = int(x0 - half_brush_size) + start_y = int(y0 - half_brush_size) + + stroke_image = QImage(stroke_height, stroke_width, QImage.Format.Format_RGBA8888) + stroke_image.fill(QColor(255,255,255,0)) + + painter = QPainter(stroke_image) + painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) + + # rotate the brush-image to follow the stroke-direction + transform = painter.transform() + transform.translate(0, stroke_width / 2) # translate to match the brush-alignment + transform.rotate(-numpy.degrees(angle)) + painter.setTransform(transform) + + # tile the brush along the stroke-length + brush_stride = max(1, half_brush_size) + for i in range(0, int(distance) + brush_stride, brush_stride): + painter.drawImage(i, -stroke_width, self._brush_image) + painter.end() + + return stroke_image, (start_x, start_y) def setPaintType(self, paint_type: str) -> None: Logger.warning(f"TODO: Implement paint-types ({paint_type}).") - pass + pass # FIXME: ... and also please call `self._stroke_image = self._createBrushStrokeImage()` (see other funcs). def setBrushSize(self, brush_size: float) -> None: - self._brush_size = int(brush_size) + if brush_size != self._brush_size: + self._brush_size = int(brush_size) + self._brush_image = self._createBrushImage() def setBrushColor(self, brush_color: str) -> None: - self._brush_color = brush_color + if brush_color != self._brush_color: + self._brush_color = brush_color + self._brush_image = self._createBrushImage() def setBrushShape(self, brush_shape: str) -> None: - self._brush_shape = brush_shape + if brush_shape != self._brush_shape: + self._brush_shape = brush_shape + self._brush_image = self._createBrushImage() @staticmethod def _get_intersect_ratio_via_pt(a: numpy.ndarray, pt: numpy.ndarray, b: numpy.ndarray, c: numpy.ndarray) -> float: @@ -85,28 +145,10 @@ class PaintTool(Tool): def _nodeTransformChanged(self, *args) -> None: self._cache_dirty = True - def _getBrushPixels(self, mid_x: float, mid_y: float, w: float, h: float) -> List[Tuple[float, float]]: - res = [] - include = False - for y in range(-self._brush_size//2, (self._brush_size + 1)//2): - for x in range(-self._brush_size//2, (self._brush_size + 1)//2): - match self._brush_shape: - case "A": - include = True - case "B": - include = x * x + y * y <= self._brush_size * self._brush_size - if include: - xx = mid_x + (x / w) - yy = mid_y + (y / h) - if xx < 0 or xx > 1 or yy < 0 or yy > 1: - continue - res.append((xx, yy)) - return res - - def _handleMouseAction(self, node: SceneNode, paintview: View, x: int, y: int) -> bool: + def _getTexCoordsFromClick(self, node: SceneNode, x: int, y: int) -> Optional[Tuple[float, float]]: face_id = self._selection_pass.getFaceIdAtPosition(x, y) if face_id < 0: - return False + return None pt = self._picking_pass.getPickedPosition(x, y).getData() @@ -123,13 +165,7 @@ class PaintTool(Tool): wb /= wt wc /= wt texcoords = wa * ta + wb * tb + wc * tc - - color = self._color_str_to_rgba[self._brush_color] - w, h = paintview.getUvTexDimensions() - for (x, y) in self._getBrushPixels(texcoords[0], texcoords[1], float(w), float(h)): - paintview.setUvPixel(x, y, color) - - return True + return texcoords def event(self, event: Event) -> bool: """Handle mouse and keyboard events. @@ -160,7 +196,7 @@ class PaintTool(Tool): if MouseEvent.LeftButton not in cast(MouseEvent, event).buttons: return False self._mouse_held = False - self._last_mouse_drag = None + self._last_text_coords = None return True is_moved = event.type == Event.MouseMoveEvent @@ -175,12 +211,11 @@ class PaintTool(Tool): return False else: self._mouse_held = True - drags = ([self._last_mouse_drag] if self._last_mouse_drag else []) + [(evt.x, evt.y)] - self._last_mouse_drag = (evt.x, evt.y) paintview = controller.getActiveView() if paintview is None or paintview.getPluginId() != "PaintTool": return False + paintview = cast(PaintView, paintview) if not self._selection_pass: return False @@ -205,7 +240,6 @@ class PaintTool(Tool): return False evt = cast(MouseEvent, event) - drags.append((evt.x, evt.y)) if not self._picking_pass: self._picking_pass = PickingPass(camera.getViewportWidth(), camera.getViewportHeight()) @@ -213,11 +247,23 @@ class PaintTool(Tool): self._selection_pass.renderFacesMode() - res = False - for (x, y) in drags: - res |= self._handleMouseAction(node, paintview, x, y) - if res: - Application.getInstance().getController().getScene().sceneChanged.emit(node) - return res + texcoords = self._getTexCoordsFromClick(node, evt.x, evt.y) + if texcoords is None: + return False + if self._last_text_coords is None: + self._last_text_coords = texcoords + + w, h = paintview.getUvTexDimensions() + sub_image, (start_x, start_y) = self._createStrokeImage( + self._last_text_coords[0] * w, + self._last_text_coords[1] * h, + texcoords[0] * w, + texcoords[1] * h + ) + paintview.addStroke(sub_image, start_x, start_y) + + self._last_text_coords = texcoords + Application.getInstance().getController().getScene().sceneChanged.emit(node) + return True return False diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index 0923d007d7..b86d59b9df 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -1,6 +1,8 @@ # Copyright (c) 2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. + import os +from PyQt6.QtGui import QImage from UM.PluginRegistry import PluginRegistry from UM.View.View import View @@ -18,8 +20,10 @@ class PaintView(View): super().__init__() self._paint_shader = None self._paint_texture = None - self._tex_width = 256 - self._tex_height = 256 + + # FIXME: When the texture UV-unwrapping is done, these two values will need to be set to a proper value (suggest 4096 for both). + self._tex_width = 512 + self._tex_height = 512 def _checkSetup(self): if not self._paint_shader: @@ -29,8 +33,8 @@ class PaintView(View): self._paint_texture = OpenGL.getInstance().createTexture(self._tex_width, self._tex_height) self._paint_shader.setTexture(0, self._paint_texture) - def setUvPixel(self, x, y, color) -> None: - self._paint_texture.setPixel(x, y, color) + def addStroke(self, stroke_image: QImage, start_x: int, start_y: int) -> None: + self._paint_texture.setSubImage(stroke_image, start_x, start_y) def getUvTexDimensions(self): return self._tex_width, self._tex_height From 4e5b0115ea39e01dee33c9d11a3a1c5d81244f9d Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 28 May 2025 14:39:07 +0200 Subject: [PATCH 019/159] Painting: Separate brush image didn't work properly, construct stroke-image by pen instead. This also simplifies things nicely. part of CURA-12543 --- plugins/PaintTool/PaintTool.py | 73 ++++++++++++---------------------- 1 file changed, 25 insertions(+), 48 deletions(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index 90482d0055..648ad9e1b3 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -3,7 +3,7 @@ import numpy from PyQt6.QtCore import Qt -from PyQt6.QtGui import QImage, QPainter, QColor, QBrush +from PyQt6.QtGui import QImage, QPainter, QColor, QBrush, QPen from typing import cast, Dict, List, Optional, Tuple from UM.Application import Application @@ -18,8 +18,7 @@ from .PaintView import PaintView class PaintTool(Tool): - """Provides the tool to paint meshes. - """ + """Provides the tool to paint meshes.""" def __init__(self) -> None: super().__init__() @@ -42,82 +41,60 @@ class PaintTool(Tool): self._brush_size: int = 10 self._brush_color: str = "A" self._brush_shape: str = "A" - self._brush_image = self._createBrushImage() + self._brush_pen: Optional[QPen] = None self._mouse_held: bool = False self._last_text_coords: Optional[Tuple[int, int]] = None - def _createBrushImage(self) -> QImage: - brush_image = QImage(self._brush_size, self._brush_size, QImage.Format.Format_RGBA8888) - brush_image.fill(QColor(255,255,255,0)) - + def _createBrushPen(self) -> QPen: + pen = QPen() + pen.setWidth(self._brush_size) color = self._color_str_to_rgba[self._brush_color] - qcolor = QColor(color[0], color[1], color[2], color[3]) - - painter = QPainter(brush_image) - painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) - painter.setPen(Qt.PenStyle.NoPen) - painter.setBrush(QBrush(qcolor)) + pen.setColor(QColor(color[0], color[1], color[2], color[3])) match self._brush_shape: - case "A": # Square brush - painter.drawRect(0, 0, self._brush_size, self._brush_size) - case "B": # Circle brush - painter.drawEllipse(0, 0, self._brush_size, self._brush_size) - case _: - painter.drawRect(0, 0, self._brush_size, self._brush_size) - painter.end() - - return brush_image + case "A": + pen.setCapStyle(Qt.PenCapStyle.SquareCap) + case "B": + pen.setCapStyle(Qt.PenCapStyle.RoundCap) + return pen def _createStrokeImage(self, x0: float, y0: float, x1: float, y1: float) -> Tuple[QImage, Tuple[int, int]]: - distance = numpy.hypot(x1 - x0, y1 - y0) - angle = numpy.arctan2(y1 - y0, x1 - x0) - stroke_width = self._brush_size - stroke_height = int(distance) + self._brush_size + xdiff = int(x1 - x0) + ydiff = int(y1 - y0) half_brush_size = self._brush_size // 2 - start_x = int(x0 - half_brush_size) - start_y = int(y0 - half_brush_size) + start_x = int(min(x0, x1) - half_brush_size) + start_y = int(min(y0, y1) - half_brush_size) - stroke_image = QImage(stroke_height, stroke_width, QImage.Format.Format_RGBA8888) - stroke_image.fill(QColor(255,255,255,0)) + stroke_image = QImage(abs(xdiff) + self._brush_size, abs(ydiff) + self._brush_size, QImage.Format.Format_RGBA8888) + stroke_image.fill(QColor(0,0,0,0)) painter = QPainter(stroke_image) - painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) - - # rotate the brush-image to follow the stroke-direction - transform = painter.transform() - transform.translate(0, stroke_width / 2) # translate to match the brush-alignment - transform.rotate(-numpy.degrees(angle)) - painter.setTransform(transform) - - # tile the brush along the stroke-length - brush_stride = max(1, half_brush_size) - for i in range(0, int(distance) + brush_stride, brush_stride): - painter.drawImage(i, -stroke_width, self._brush_image) + painter.setPen(self._brush_pen) + painter.drawLine(int(x0 - start_x), int(y0 - start_y), int(x1 - start_x), int(y1 - start_y)) painter.end() return stroke_image, (start_x, start_y) def setPaintType(self, paint_type: str) -> None: Logger.warning(f"TODO: Implement paint-types ({paint_type}).") - pass # FIXME: ... and also please call `self._stroke_image = self._createBrushStrokeImage()` (see other funcs). + pass # FIXME: ... and also please call `self._brush_pen = self._createBrushPen()` (see other funcs). def setBrushSize(self, brush_size: float) -> None: if brush_size != self._brush_size: self._brush_size = int(brush_size) - self._brush_image = self._createBrushImage() + self._brush_pen = self._createBrushPen() def setBrushColor(self, brush_color: str) -> None: if brush_color != self._brush_color: self._brush_color = brush_color - self._brush_image = self._createBrushImage() + self._brush_pen = self._createBrushPen() def setBrushShape(self, brush_shape: str) -> None: if brush_shape != self._brush_shape: self._brush_shape = brush_shape - self._brush_image = self._createBrushImage() + self._brush_pen = self._createBrushPen() @staticmethod def _get_intersect_ratio_via_pt(a: numpy.ndarray, pt: numpy.ndarray, b: numpy.ndarray, c: numpy.ndarray) -> float: @@ -147,7 +124,7 @@ class PaintTool(Tool): def _getTexCoordsFromClick(self, node: SceneNode, x: int, y: int) -> Optional[Tuple[float, float]]: face_id = self._selection_pass.getFaceIdAtPosition(x, y) - if face_id < 0: + if face_id < 0 or face_id >= node.getMeshData().getFaceCount(): return None pt = self._picking_pass.getPickedPosition(x, y).getData() From 50ea216a608a943f9fb0f03e7f9e7ec6882d9efc Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 28 May 2025 15:13:22 +0200 Subject: [PATCH 020/159] Store UV coordinates to 3MF file CURA-12544 --- plugins/3MFWriter/ThreeMFWriter.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index 558b36576f..1cab5c6b71 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -109,7 +109,8 @@ class ThreeMFWriter(MeshWriter): def _convertUMNodeToSavitarNode(um_node, transformation = Matrix(), exported_settings: Optional[Dict[str, Set[str]]] = None, - center_mesh = False): + center_mesh = False, + scene: Savitar.Scene = None): """Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode :returns: Uranium Scene node. @@ -150,7 +151,11 @@ class ThreeMFWriter(MeshWriter): if indices_array is not None: savitar_node.getMeshData().setFacesFromBytes(indices_array) else: - savitar_node.getMeshData().setFacesFromBytes(numpy.arange(mesh_data.getVertices().size / 3, dtype=numpy.int32).tostring()) + savitar_node.getMeshData().setFacesFromBytes(numpy.arange(mesh_data.getVertices().size / 3, dtype=numpy.int32).tobytes()) + + uv_coordinates_array = mesh_data.getUVCoordinatesAsByteArray() + if uv_coordinates_array is not None and len(uv_coordinates_array) > 0: + savitar_node.getMeshData().setUVCoordinatesPerVertexAsBytes(uv_coordinates_array, scene) # Handle per object settings (if any) stack = um_node.callDecoration("getStack") @@ -187,7 +192,8 @@ class ThreeMFWriter(MeshWriter): if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr: continue savitar_child_node = ThreeMFWriter._convertUMNodeToSavitarNode(child_node, - exported_settings = exported_settings) + exported_settings = exported_settings, + scene = scene) if savitar_child_node is not None: savitar_node.addChild(savitar_child_node) @@ -320,13 +326,15 @@ class ThreeMFWriter(MeshWriter): savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(root_child, transformation_matrix, exported_model_settings, - center_mesh = True) + center_mesh = True, + scene = savitar_scene) if savitar_node: savitar_scene.addSceneNode(savitar_node) else: savitar_node = self._convertUMNodeToSavitarNode(node, transformation_matrix, - exported_model_settings) + exported_model_settings, + scene = savitar_scene) if savitar_node: savitar_scene.addSceneNode(savitar_node) @@ -500,7 +508,7 @@ class ThreeMFWriter(MeshWriter): def sceneNodesToString(scene_nodes: [SceneNode]) -> str: savitar_scene = Savitar.Scene() for scene_node in scene_nodes: - savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(scene_node, center_mesh = True) + savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(scene_node, center_mesh = True, scene = savitar_scene) savitar_scene.addSceneNode(savitar_node) parser = Savitar.ThreeMFParser() scene_string = parser.sceneToString(savitar_scene) From c9ca999f106ef22207d71f50d22915b1c493a8c5 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 28 May 2025 16:43:33 +0200 Subject: [PATCH 021/159] PaintTool: Undo/Redo should be working now. Also fix missing pen-shape I suppose. part of CURA-12543 --- plugins/PaintTool/PaintTool.py | 49 ++++++++++++++++++++++++++---- plugins/PaintTool/PaintTool.qml | 35 +++++++++++++++++++++ plugins/PaintTool/PaintView.py | 54 ++++++++++++++++++++++++++++++--- 3 files changed, 128 insertions(+), 10 deletions(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index 648ad9e1b3..cf87772d0b 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -41,9 +41,12 @@ class PaintTool(Tool): self._brush_size: int = 10 self._brush_color: str = "A" self._brush_shape: str = "A" - self._brush_pen: Optional[QPen] = None + self._brush_pen: QPen = self._createBrushPen() self._mouse_held: bool = False + self._ctrl_held: bool = False + self._shift_held: bool = False + self._last_text_coords: Optional[Tuple[int, int]] = None def _createBrushPen(self) -> QPen: @@ -96,6 +99,20 @@ class PaintTool(Tool): self._brush_shape = brush_shape self._brush_pen = self._createBrushPen() + def undoStackAction(self, redo_instead: bool) -> bool: + paintview = Application.getInstance().getController().getActiveView() + if paintview is None or paintview.getPluginId() != "PaintTool": + return False + paintview = cast(PaintView, paintview) + if redo_instead: + paintview.redoStroke() + else: + paintview.undoStroke() + nodes = Selection.getAllSelectedObjects() + if len(nodes) > 0: + Application.getInstance().getController().getScene().sceneChanged.emit(nodes[0]) + return True + @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 @@ -154,6 +171,10 @@ class PaintTool(Tool): super().event(event) controller = Application.getInstance().getController() + nodes = Selection.getAllSelectedObjects() + if len(nodes) <= 0: + return False + node = nodes[0] # Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes if event.type == Event.ToolActivateEvent: @@ -166,7 +187,27 @@ class PaintTool(Tool): controller.setActiveView("SolidView") return True - if event.type == Event.KeyPressEvent and cast(KeyEvent, event).key == KeyEvent.ShiftKey: + if event.type == Event.KeyPressEvent: + evt = cast(KeyEvent, event) + if evt.key == KeyEvent.ControlKey: + self._ctrl_held = True + return True + if evt.key == KeyEvent.ShiftKey: + self._shift_held = True + return True + return False + + if event.type == Event.KeyReleaseEvent: + evt = cast(KeyEvent, event) + if evt.key == KeyEvent.ControlKey: + self._ctrl_held = False + return True + if evt.key == KeyEvent.ShiftKey: + self._shift_held = False + return True + if evt.key == Qt.Key.Key_L and self._ctrl_held: + # NOTE: Ctrl-L is used here instead of Ctrl-Z, as the latter is the application-wide one that takes precedence. + return self.undoStackAction(self._shift_held) return False if event.type == Event.MouseReleaseEvent and self._controller.getToolsEnabled(): @@ -201,10 +242,6 @@ class PaintTool(Tool): if not camera: return False - node = Selection.getAllSelectedObjects()[0] - if node is None: - return False - if node != self._node_cache: if self._node_cache is not None: self._node_cache.transformationChanged.disconnect(self._nodeTransformChanged) diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml index 82d6d90ffd..a1fac9c3a3 100644 --- a/plugins/PaintTool/PaintTool.qml +++ b/plugins/PaintTool/PaintTool.qml @@ -176,5 +176,40 @@ Item } } } + + RowLayout + { + UM.ToolbarButton + { + id: undoButton + + text: catalog.i18nc("@action:button", "Undo Stroke") + toolItem: UM.ColorImage + { + source: UM.Theme.getIcon("ArrowReset") + } + property bool needBorder: true + + z: 2 + + onClicked: UM.Controller.triggerActionWithData("undoStackAction", false) + } + + UM.ToolbarButton + { + id: redoButton + + text: catalog.i18nc("@action:button", "Redo Stroke") + toolItem: UM.ColorImage + { + source: UM.Theme.getIcon("ArrowDoubleCircleRight") + } + property bool needBorder: true + + z: 2 + + onClicked: UM.Controller.triggerActionWithData("undoStackAction", true) + } + } } } diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index b86d59b9df..5fb62436c6 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -2,9 +2,13 @@ # Cura is released under the terms of the LGPLv3 or higher. import os -from PyQt6.QtGui import QImage +from typing import Optional, List, Tuple + +from PyQt6.QtGui import QImage, QColor, QPainter 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.Selection import Selection from UM.View.GL.OpenGL import OpenGL @@ -16,15 +20,23 @@ catalog = i18nCatalog("cura") class PaintView(View): """View for model-painting.""" + UNDO_STACK_SIZE = 1024 + def __init__(self) -> None: super().__init__() - self._paint_shader = None - self._paint_texture = None + self._paint_shader: Optional[ShaderProgram] = None + self._paint_texture: Optional[Texture] = None # FIXME: When the texture UV-unwrapping is done, these two values will need to be set to a proper value (suggest 4096 for both). self._tex_width = 512 self._tex_height = 512 + self._stroke_undo_stack: List[Tuple[QImage, int, int]] = [] + self._stroke_redo_stack: List[Tuple[QImage, int, int]] = [] + + self._force_opaque_mask = QImage(2, 2, QImage.Format.Format_Mono) + self._force_opaque_mask.fill(1) + def _checkSetup(self): if not self._paint_shader: shader_filename = os.path.join(PluginRegistry.getInstance().getPluginPath("PaintTool"), "paint.shader") @@ -33,8 +45,42 @@ class PaintView(View): self._paint_texture = OpenGL.getInstance().createTexture(self._tex_width, self._tex_height) self._paint_shader.setTexture(0, self._paint_texture) + 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_image: QImage, start_x: int, start_y: int) -> None: - self._paint_texture.setSubImage(stroke_image, start_x, start_y) + 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._paint_texture.setSubImage(stroke_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: + return False + from_image, x, y = from_stack.pop() + to_image = self._forceOpaqueDeepCopy(self._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 getUvTexDimensions(self): return self._tex_width, self._tex_height From d28c2aac68950efab787353aaa5c3a62ede2033d Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 28 May 2025 17:12:42 +0200 Subject: [PATCH 022/159] Painting: Fix non-drag not producing a circle (square was already OK though). part of CURA-12543 --- plugins/PaintTool/PaintTool.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index cf87772d0b..2f6d3736f3 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -75,7 +75,10 @@ class PaintTool(Tool): painter = QPainter(stroke_image) painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) painter.setPen(self._brush_pen) - painter.drawLine(int(x0 - start_x), int(y0 - start_y), int(x1 - start_x), int(y1 - start_y)) + if xdiff == 0 and ydiff == 0: + painter.drawPoint(int(x0 - start_x), int(y0 - start_y)) + else: + painter.drawLine(int(x0 - start_x), int(y0 - start_y), int(x1 - start_x), int(y1 - start_y)) painter.end() return stroke_image, (start_x, start_y) From f0764134cc174a4e8ef8fdf70c0f53fe22a99345 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 3 Jun 2025 13:27:57 +0200 Subject: [PATCH 023/159] Store painted texture to 3MF file CURA-12544 Also allows having multiple texture for multiple models while painting --- cura/Scene/SliceableObjectDecorator.py | 11 + plugins/3MFWriter/ThreeMFWriter.py | 55 +- plugins/PaintTool/PaintView.py | 19 +- .../themes/cura-dark-colorblind/theme.json | 27 +- resources/themes/cura-dark/theme.json | 213 +----- .../themes/cura-light-colorblind/theme.json | 30 +- resources/themes/cura-light/theme.json | 690 +----------------- resources/themes/daily_test_colors.json | 16 - 8 files changed, 73 insertions(+), 988 deletions(-) delete mode 100644 resources/themes/daily_test_colors.json diff --git a/cura/Scene/SliceableObjectDecorator.py b/cura/Scene/SliceableObjectDecorator.py index ad51f7d755..a52f7badf4 100644 --- a/cura/Scene/SliceableObjectDecorator.py +++ b/cura/Scene/SliceableObjectDecorator.py @@ -1,12 +1,23 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator +from UM.View.GL.OpenGL import OpenGL +# FIXME: When the texture UV-unwrapping is done, these two values will need to be set to a proper value (suggest 4096 for both). +TEXTURE_WIDTH = 512 +TEXTURE_HEIGHT = 512 + class SliceableObjectDecorator(SceneNodeDecorator): def __init__(self) -> None: super().__init__() + self._paint_texture = None def isSliceable(self) -> bool: return True + def getPaintTexture(self, create_if_required: bool = True): + if self._paint_texture is None and create_if_required: + self._paint_texture = OpenGL.getInstance().createTexture(TEXTURE_WIDTH, TEXTURE_HEIGHT) + return self._paint_texture + def __deepcopy__(self, memo) -> "SliceableObjectDecorator": return type(self)() diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index 1cab5c6b71..4cb7840841 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -58,6 +58,8 @@ catalog = i18nCatalog("cura") MODEL_PATH = "3D/3dmodel.model" PACKAGE_METADATA_PATH = "Cura/packages.json" +TEXTURES_PATH = "3D/Textures" +MODEL_RELATIONS_PATH = "3D/_rels/3dmodel.model.rels" class ThreeMFWriter(MeshWriter): def __init__(self): @@ -110,7 +112,10 @@ class ThreeMFWriter(MeshWriter): transformation = Matrix(), exported_settings: Optional[Dict[str, Set[str]]] = None, center_mesh = False, - scene: Savitar.Scene = None): + scene: Savitar.Scene = None, + archive: zipfile.ZipFile = None, + model_relations_element: ET.Element = None, + content_types_element: ET.Element = None): """Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode :returns: Uranium Scene node. @@ -153,9 +158,33 @@ class ThreeMFWriter(MeshWriter): else: savitar_node.getMeshData().setFacesFromBytes(numpy.arange(mesh_data.getVertices().size / 3, dtype=numpy.int32).tobytes()) + texture = um_node.callDecoration("getPaintTexture") uv_coordinates_array = mesh_data.getUVCoordinatesAsByteArray() - if uv_coordinates_array is not None and len(uv_coordinates_array) > 0: - savitar_node.getMeshData().setUVCoordinatesPerVertexAsBytes(uv_coordinates_array, scene) + if texture is not None and archive is not None and uv_coordinates_array is not None and len(uv_coordinates_array) > 0: + texture_image = texture.getImage() + if texture_image is not None: + texture_path = f"{TEXTURES_PATH}/{id(um_node)}.png" + + texture_buffer = QBuffer() + texture_buffer.open(QBuffer.OpenModeFlag.ReadWrite) + texture_image.save(texture_buffer, "PNG") + + texture_file = zipfile.ZipInfo(texture_path) + # Don't try to compress texture file, because the PNG is pretty much as compact as it will get + archive.writestr(texture_file, texture_buffer.data()) + + savitar_node.getMeshData().setUVCoordinatesPerVertexAsBytes(uv_coordinates_array, texture_path, scene) + + # Add texture relation to model relations file + if model_relations_element is not None: + ET.SubElement(model_relations_element, "Relationship", + Target=texture_path, Id=f"rel{len(model_relations_element)+1}", + Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dtexture") + + if content_types_element is not None: + ET.SubElement(content_types_element, "Override", PartName=texture_path, + ContentType="application/vnd.ms-package.3dmanufacturing-3dmodeltexture") + # Handle per object settings (if any) stack = um_node.callDecoration("getStack") @@ -193,7 +222,10 @@ class ThreeMFWriter(MeshWriter): continue savitar_child_node = ThreeMFWriter._convertUMNodeToSavitarNode(child_node, exported_settings = exported_settings, - scene = scene) + scene = scene, + archive = archive, + model_relations_element = model_relations_element, + content_types_element = content_types_element) if savitar_child_node is not None: savitar_node.addChild(savitar_child_node) @@ -255,6 +287,9 @@ class ThreeMFWriter(MeshWriter): # Create Metadata/_rels/model_settings.config.rels metadata_relations_element = self._makeRelationsTree() + # Create model relations + model_relations_element = self._makeRelationsTree() + # Let the variant add its specific files variant.add_extra_files(archive, metadata_relations_element) @@ -327,14 +362,20 @@ class ThreeMFWriter(MeshWriter): transformation_matrix, exported_model_settings, center_mesh = True, - scene = savitar_scene) + scene = savitar_scene, + archive = archive, + model_relations_element = model_relations_element, + content_types_element = content_types) if savitar_node: savitar_scene.addSceneNode(savitar_node) else: savitar_node = self._convertUMNodeToSavitarNode(node, transformation_matrix, exported_model_settings, - scene = savitar_scene) + scene = savitar_scene, + archive = archive, + model_relations_element = model_relations_element, + content_types_element = content_types) if savitar_node: savitar_scene.addSceneNode(savitar_node) @@ -346,6 +387,8 @@ class ThreeMFWriter(MeshWriter): self._storeElementTree(archive, "_rels/.rels", relations_element) if len(metadata_relations_element) > 0: self._storeElementTree(archive, "Metadata/_rels/model_settings.config.rels", metadata_relations_element) + if len(model_relations_element) > 0: + self._storeElementTree(archive, MODEL_RELATIONS_PATH, model_relations_element) except Exception as error: Logger.logException("e", "Error writing zip file") self.setInformation(str(error)) diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index b86d59b9df..c50d957ee2 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -19,25 +19,21 @@ class PaintView(View): def __init__(self) -> None: super().__init__() self._paint_shader = None - self._paint_texture = None - - # FIXME: When the texture UV-unwrapping is done, these two values will need to be set to a proper value (suggest 4096 for both). - self._tex_width = 512 - self._tex_height = 512 + self._current_paint_texture = None 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) - if not self._paint_texture: - self._paint_texture = OpenGL.getInstance().createTexture(self._tex_width, self._tex_height) - self._paint_shader.setTexture(0, self._paint_texture) def addStroke(self, stroke_image: QImage, start_x: int, start_y: int) -> None: - self._paint_texture.setSubImage(stroke_image, start_x, start_y) + if self._current_paint_texture is not None: + self._current_paint_texture.setSubImage(stroke_image, start_x, start_y) def getUvTexDimensions(self): - return self._tex_width, self._tex_height + if self._current_paint_texture is not None: + return self._current_paint_texture.getWidth(), self._current_paint_texture.getHeight() + return 0, 0 def beginRendering(self) -> None: renderer = self.getRenderer() @@ -48,4 +44,7 @@ class PaintView(View): node = Selection.getAllSelectedObjects()[0] if node is None: return + + 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()) diff --git a/resources/themes/cura-dark-colorblind/theme.json b/resources/themes/cura-dark-colorblind/theme.json index 4a006ee836..4443111b60 100644 --- a/resources/themes/cura-dark-colorblind/theme.json +++ b/resources/themes/cura-dark-colorblind/theme.json @@ -1,26 +1 @@ -{ - "metadata": { - "name": "Colorblind Assist Dark", - "inherits": "cura-dark" - }, - - "colors": { - "x_axis": [212, 0, 0, 255], - "y_axis": [64, 64, 255, 255], - - "model_overhang": [200, 0, 255, 255], - - "xray": [26, 26, 62, 255], - "xray_error": [255, 0, 0, 255], - - "layerview_inset_0": [255, 64, 0, 255], - "layerview_inset_x": [0, 156, 128, 255], - "layerview_skin": [255, 255, 86, 255], - "layerview_support": [255, 255, 0, 255], - - "layerview_infill": [0, 255, 255, 255], - "layerview_support_infill": [0, 200, 200, 255], - - "layerview_move_retraction": [0, 100, 255, 255] - } -} +{"metadata": {"name": "Colorblind Assist Dark", "inherits": "cura-dark"}, "colors": {"x_axis": [212, 0, 0, 255], "y_axis": [64, 64, 255, 255], "model_overhang": [200, 0, 255, 255], "xray": [26, 26, 62, 255], "xray_error": [255, 0, 0, 255], "layerview_inset_0": [255, 64, 0, 255], "layerview_inset_x": [0, 156, 128, 255], "layerview_skin": [255, 255, 86, 255], "layerview_support": [255, 255, 0, 255], "layerview_infill": [0, 255, 255, 255], "layerview_support_infill": [0, 200, 200, 255], "layerview_move_retraction": [0, 100, 255, 255], "main_window_header_background": [192, 199, 65, 255]}} \ No newline at end of file diff --git a/resources/themes/cura-dark/theme.json b/resources/themes/cura-dark/theme.json index 1517b22eb9..29be47e697 100644 --- a/resources/themes/cura-dark/theme.json +++ b/resources/themes/cura-dark/theme.json @@ -1,212 +1 @@ -{ - "metadata": { - "name": "UltiMaker Dark", - "inherits": "cura-light" - }, - - "base_colors": - { - "background_1": [31, 31, 32, 255], - "background_2": [57, 57, 58, 255], - "background_3": [85, 85, 87, 255], - "background_4": [23, 23, 23, 255], - - "accent_1": [25, 110, 240, 255], - "accent_2": [16, 70, 156, 255], - "border_main": [118, 118, 119, 255], - "border_accent_1": [255, 255, 255, 255], - "border_accent_2": [243, 243, 243, 255], - "border_field": [57, 57, 58, 255], - - "text_default": [255, 255, 255, 255], - "text_disabled": [118, 118, 118, 255], - "text_primary_button": [255, 255, 255, 255], - "text_secondary_button": [255, 255, 255, 255], - "text_link_hover": [156, 195, 255, 255], - "text_lighter": [243, 243, 243, 255], - - "um_green_1": [233, 245, 237, 255], - "um_green_5": [36, 162, 73, 255], - "um_green_9": [31, 44, 36, 255], - "um_red_1": [251, 232, 233, 255], - "um_red_5": [218, 30, 40, 255], - "um_red_9": [59, 31, 33, 255], - "um_orange_1": [255, 235, 221, 255], - "um_orange_5": [252, 123, 30, 255], - "um_orange_9": [64, 45, 32, 255], - "um_yellow_1": [255, 248, 225, 255], - "um_yellow_5": [253, 209, 58, 255], - "um_yellow_9": [64, 58, 36, 255] - }, - - "colors": { - "main_background": "background_1", - "detail_background": "background_2", - "message_background": "background_1", - "wide_lining": [31, 36, 39, 255], - "thick_lining": [255, 255, 255, 60], - "lining": "border_main", - "viewport_overlay": "background_1", - - "primary_text": "text_default", - "secondary": [95, 95, 95, 255], - - "expandable_active": "background_2", - "expandable_hover": "background_2", - - "secondary_button": "background_1", - "secondary_button_hover": "background_3", - "secondary_button_text": "text_secondary_button", - - "icon": "text_default", - "toolbar_background": "background_1", - "toolbar_button_active": "background_3", - "toolbar_button_hover": "background_3", - "toolbar_button_active_hover": "background_3", - - "main_window_header_button_background_inactive": "background_4", - "main_window_header_button_text_inactive": "text_primary_button", - "main_window_header_button_text_active": "background_4", - "main_window_header_background": "background_4", - "main_window_header_background_gradient": "background_4", - "main_window_header_button_background_hovered": [46, 46, 46, 255], - - "account_sync_state_icon": [255, 255, 255, 204], - - "machine_selector_printer_icon": [204, 204, 204, 255], - - "text": "text_default", - "text_detail": [255, 255, 255, 172], - "text_link": "accent_1", - "text_inactive": [118, 118, 118, 255], - "text_hover": [255, 255, 255, 255], - "text_scene": [250, 250, 250, 255], - "text_scene_hover": [255, 255, 255, 255], - - "printer_type_label_background": [95, 95, 95, 255], - - "error": [212, 31, 53, 255], - "disabled": [32, 32, 32, 255], - - "button": [39, 44, 48, 255], - "button_hover": [39, 44, 48, 255], - "button_text": "text_default", - "button_disabled": [39, 44, 48, 255], - "button_disabled_text": [255, 255, 255, 101], - - "small_button_text": [255, 255, 255, 197], - "small_button_text_hover": [255, 255, 255, 255], - - "button_tooltip": [39, 44, 48, 255], - - "tab_checked": [39, 44, 48, 255], - "tab_checked_border": [255, 255, 255, 30], - "tab_checked_text": [255, 255, 255, 255], - "tab_unchecked": [39, 44, 48, 255], - "tab_unchecked_border": [255, 255, 255, 30], - "tab_unchecked_text": [255, 255, 255, 101], - "tab_hovered": [39, 44, 48, 255], - "tab_hovered_border": [255, 255, 255, 30], - "tab_hovered_text": [255, 255, 255, 255], - "tab_active": [39, 44, 48, 255], - "tab_active_border": [255, 255, 255, 30], - "tab_active_text": [255, 255, 255, 255], - "tab_background": [39, 44, 48, 255], - - "action_button": "background_1", - "action_button_text": [255, 255, 255, 200], - "action_button_border": "border_main", - "action_button_hovered": [79, 85, 89, 255], - "action_button_hovered_text": "text_default", - "action_button_hovered_border": "border_main", - "action_button_active": [39, 44, 48, 30], - "action_button_active_text": "text_default", - "action_button_active_border": [255, 255, 255, 100], - "action_button_disabled": "background_3", - "action_button_disabled_text": "text_disabled", - "action_button_disabled_border": [255, 255, 255, 30], - - "scrollbar_background": [39, 44, 48, 0], - "scrollbar_handle": [255, 255, 255, 105], - "scrollbar_handle_hover": [255, 255, 255, 255], - "scrollbar_handle_down": [255, 255, 255, 255], - - "setting_category_disabled": [75, 80, 83, 255], - "setting_category_disabled_text": [255, 255, 255, 101], - - "setting_control": "background_2", - "setting_control_selected": [34, 39, 42, 38], - "setting_control_highlight": "background_3", - "setting_control_border": [255, 255, 255, 38], - "setting_control_border_highlight": [12, 169, 227, 255], - "setting_control_text": "text_default", - "setting_control_button": [255, 255, 255, 127], - "setting_control_button_hover": [255, 255, 255, 204], - "setting_control_disabled": [34, 39, 42, 255], - "setting_control_disabled_text": [255, 255, 255, 101], - "setting_control_disabled_border": [255, 255, 255, 101], - "setting_unit": [255, 255, 255, 127], - "setting_validation_error_background": "um_red_9", - "setting_validation_warning_background": "um_yellow_9", - "setting_validation_ok": "background_2", - - "progressbar_background": [255, 255, 255, 48], - "progressbar_control": [255, 255, 255, 197], - - "slider_groove": [127, 127, 127, 255], - "slider_groove_border": [127, 127, 127, 255], - "slider_groove_fill": [245, 245, 245, 255], - "slider_handle": [255, 255, 255, 255], - "slider_handle_active": [68, 192, 255, 255], - - "category_background": "background_3", - - "tooltip": "background_2", - "tooltip_text": "text_default", - - "tool_panel_background": "background_1", - - "viewport_background": "background_1", - "volume_outline": [12, 169, 227, 128], - "buildplate": [169, 169, 169, 255], - "buildplate_grid_minor": [154, 154, 155, 255], - - "disallowed_area": [0, 0, 0, 52], - - "model_selection_outline": [12, 169, 227, 255], - - "material_compatibility_warning": [255, 255, 255, 255], - - "core_compatibility_warning": [255, 255, 255, 255], - - "quality_slider_available": [255, 255, 255, 255], - - "monitor_printer_family_tag": [86, 86, 106, 255], - "monitor_text_disabled": [102, 102, 102, 255], - "monitor_icon_primary": [229, 229, 229, 255], - "monitor_icon_accent": [51, 53, 54, 255], - "monitor_icon_disabled": [102, 102, 102, 255], - - "monitor_secondary_button_hover": [80, 80, 80, 255], - "monitor_card_border": [102, 102, 102, 255], - "monitor_card_background": [51, 53, 54, 255], - "monitor_card_hover": [84, 89, 95, 255], - - "monitor_stage_background": "background_1", - "monitor_stage_background_fade": "background_1", - - "monitor_progress_bar_deactive": [102, 102, 102, 255], - "monitor_progress_bar_empty": [67, 67, 67, 255], - - "monitor_tooltip_text": [229, 229, 229, 255], - "monitor_context_menu": [67, 67, 67, 255], - "monitor_context_menu_hover": [30, 102, 215, 255], - - "monitor_skeleton_loading": [102, 102, 102, 255], - "monitor_placeholder_image": [102, 102, 102, 255], - "monitor_shadow": [4, 10, 13, 255], - - "monitor_carousel_dot": [119, 119, 119, 255], - "monitor_carousel_dot_current": [216, 216, 216, 255] - } -} +{"metadata": {"name": "UltiMaker Dark", "inherits": "cura-light"}, "base_colors": {"background_1": [31, 31, 32, 255], "background_2": [57, 57, 58, 255], "background_3": [85, 85, 87, 255], "background_4": [23, 23, 23, 255], "accent_1": [25, 110, 240, 255], "accent_2": [16, 70, 156, 255], "border_main": [118, 118, 119, 255], "border_accent_1": [255, 255, 255, 255], "border_accent_2": [243, 243, 243, 255], "border_field": [57, 57, 58, 255], "text_default": [255, 255, 255, 255], "text_disabled": [118, 118, 118, 255], "text_primary_button": [255, 255, 255, 255], "text_secondary_button": [255, 255, 255, 255], "text_link_hover": [156, 195, 255, 255], "text_lighter": [243, 243, 243, 255], "um_green_1": [233, 245, 237, 255], "um_green_5": [36, 162, 73, 255], "um_green_9": [31, 44, 36, 255], "um_red_1": [251, 232, 233, 255], "um_red_5": [218, 30, 40, 255], "um_red_9": [59, 31, 33, 255], "um_orange_1": [255, 235, 221, 255], "um_orange_5": [252, 123, 30, 255], "um_orange_9": [64, 45, 32, 255], "um_yellow_1": [255, 248, 225, 255], "um_yellow_5": [253, 209, 58, 255], "um_yellow_9": [64, 58, 36, 255]}, "colors": {"main_background": "background_1", "detail_background": "background_2", "message_background": "background_1", "wide_lining": [31, 36, 39, 255], "thick_lining": [255, 255, 255, 60], "lining": "border_main", "viewport_overlay": "background_1", "primary_text": "text_default", "secondary": [95, 95, 95, 255], "expandable_active": "background_2", "expandable_hover": "background_2", "secondary_button": "background_1", "secondary_button_hover": "background_3", "secondary_button_text": "text_secondary_button", "icon": "text_default", "toolbar_background": "background_1", "toolbar_button_active": "background_3", "toolbar_button_hover": "background_3", "toolbar_button_active_hover": "background_3", "main_window_header_button_background_inactive": "background_4", "main_window_header_button_text_inactive": "text_primary_button", "main_window_header_button_text_active": "background_4", "main_window_header_background": [192, 199, 65, 255], "main_window_header_background_gradient": "background_4", "main_window_header_button_background_hovered": [46, 46, 46, 255], "account_sync_state_icon": [255, 255, 255, 204], "machine_selector_printer_icon": [204, 204, 204, 255], "text": "text_default", "text_detail": [255, 255, 255, 172], "text_link": "accent_1", "text_inactive": [118, 118, 118, 255], "text_hover": [255, 255, 255, 255], "text_scene": [250, 250, 250, 255], "text_scene_hover": [255, 255, 255, 255], "printer_type_label_background": [95, 95, 95, 255], "error": [212, 31, 53, 255], "disabled": [32, 32, 32, 255], "button": [39, 44, 48, 255], "button_hover": [39, 44, 48, 255], "button_text": "text_default", "button_disabled": [39, 44, 48, 255], "button_disabled_text": [255, 255, 255, 101], "small_button_text": [255, 255, 255, 197], "small_button_text_hover": [255, 255, 255, 255], "button_tooltip": [39, 44, 48, 255], "tab_checked": [39, 44, 48, 255], "tab_checked_border": [255, 255, 255, 30], "tab_checked_text": [255, 255, 255, 255], "tab_unchecked": [39, 44, 48, 255], "tab_unchecked_border": [255, 255, 255, 30], "tab_unchecked_text": [255, 255, 255, 101], "tab_hovered": [39, 44, 48, 255], "tab_hovered_border": [255, 255, 255, 30], "tab_hovered_text": [255, 255, 255, 255], "tab_active": [39, 44, 48, 255], "tab_active_border": [255, 255, 255, 30], "tab_active_text": [255, 255, 255, 255], "tab_background": [39, 44, 48, 255], "action_button": "background_1", "action_button_text": [255, 255, 255, 200], "action_button_border": "border_main", "action_button_hovered": [79, 85, 89, 255], "action_button_hovered_text": "text_default", "action_button_hovered_border": "border_main", "action_button_active": [39, 44, 48, 30], "action_button_active_text": "text_default", "action_button_active_border": [255, 255, 255, 100], "action_button_disabled": "background_3", "action_button_disabled_text": "text_disabled", "action_button_disabled_border": [255, 255, 255, 30], "scrollbar_background": [39, 44, 48, 0], "scrollbar_handle": [255, 255, 255, 105], "scrollbar_handle_hover": [255, 255, 255, 255], "scrollbar_handle_down": [255, 255, 255, 255], "setting_category_disabled": [75, 80, 83, 255], "setting_category_disabled_text": [255, 255, 255, 101], "setting_control": "background_2", "setting_control_selected": [34, 39, 42, 38], "setting_control_highlight": "background_3", "setting_control_border": [255, 255, 255, 38], "setting_control_border_highlight": [12, 169, 227, 255], "setting_control_text": "text_default", "setting_control_button": [255, 255, 255, 127], "setting_control_button_hover": [255, 255, 255, 204], "setting_control_disabled": [34, 39, 42, 255], "setting_control_disabled_text": [255, 255, 255, 101], "setting_control_disabled_border": [255, 255, 255, 101], "setting_unit": [255, 255, 255, 127], "setting_validation_error_background": "um_red_9", "setting_validation_warning_background": "um_yellow_9", "setting_validation_ok": "background_2", "progressbar_background": [255, 255, 255, 48], "progressbar_control": [255, 255, 255, 197], "slider_groove": [127, 127, 127, 255], "slider_groove_border": [127, 127, 127, 255], "slider_groove_fill": [245, 245, 245, 255], "slider_handle": [255, 255, 255, 255], "slider_handle_active": [68, 192, 255, 255], "category_background": "background_3", "tooltip": "background_2", "tooltip_text": "text_default", "tool_panel_background": "background_1", "viewport_background": "background_1", "volume_outline": [12, 169, 227, 128], "buildplate": [169, 169, 169, 255], "buildplate_grid_minor": [154, 154, 155, 255], "disallowed_area": [0, 0, 0, 52], "model_selection_outline": [12, 169, 227, 255], "material_compatibility_warning": [255, 255, 255, 255], "core_compatibility_warning": [255, 255, 255, 255], "quality_slider_available": [255, 255, 255, 255], "monitor_printer_family_tag": [86, 86, 106, 255], "monitor_text_disabled": [102, 102, 102, 255], "monitor_icon_primary": [229, 229, 229, 255], "monitor_icon_accent": [51, 53, 54, 255], "monitor_icon_disabled": [102, 102, 102, 255], "monitor_secondary_button_hover": [80, 80, 80, 255], "monitor_card_border": [102, 102, 102, 255], "monitor_card_background": [51, 53, 54, 255], "monitor_card_hover": [84, 89, 95, 255], "monitor_stage_background": "background_1", "monitor_stage_background_fade": "background_1", "monitor_progress_bar_deactive": [102, 102, 102, 255], "monitor_progress_bar_empty": [67, 67, 67, 255], "monitor_tooltip_text": [229, 229, 229, 255], "monitor_context_menu": [67, 67, 67, 255], "monitor_context_menu_hover": [30, 102, 215, 255], "monitor_skeleton_loading": [102, 102, 102, 255], "monitor_placeholder_image": [102, 102, 102, 255], "monitor_shadow": [4, 10, 13, 255], "monitor_carousel_dot": [119, 119, 119, 255], "monitor_carousel_dot_current": [216, 216, 216, 255]}} \ No newline at end of file diff --git a/resources/themes/cura-light-colorblind/theme.json b/resources/themes/cura-light-colorblind/theme.json index 740bf977b2..cc7ed5dfba 100644 --- a/resources/themes/cura-light-colorblind/theme.json +++ b/resources/themes/cura-light-colorblind/theme.json @@ -1,29 +1 @@ -{ - "metadata": { - "name": "Colorblind Assist Light", - "inherits": "cura-light" - }, - - "colors": { - - "x_axis": [200, 0, 0, 255], - "y_axis": [64, 64, 255, 255], - "model_overhang": [200, 0, 255, 255], - "model_selection_outline": [12, 169, 227, 255], - - "xray_error_dark": [255, 0, 0, 255], - "xray_error_light": [255, 255, 0, 255], - "xray": [26, 26, 62, 255], - "xray_error": [255, 0, 0, 255], - - "layerview_inset_0": [255, 64, 0, 255], - "layerview_inset_x": [0, 156, 128, 255], - "layerview_skin": [255, 255, 86, 255], - "layerview_support": [255, 255, 0, 255], - - "layerview_infill": [0, 255, 255, 255], - "layerview_support_infill": [0, 200, 200, 255], - - "layerview_move_retraction": [0, 100, 255, 255] - } -} +{"metadata": {"name": "Colorblind Assist Light", "inherits": "cura-light"}, "colors": {"x_axis": [200, 0, 0, 255], "y_axis": [64, 64, 255, 255], "model_overhang": [200, 0, 255, 255], "model_selection_outline": [12, 169, 227, 255], "xray_error_dark": [255, 0, 0, 255], "xray_error_light": [255, 255, 0, 255], "xray": [26, 26, 62, 255], "xray_error": [255, 0, 0, 255], "layerview_inset_0": [255, 64, 0, 255], "layerview_inset_x": [0, 156, 128, 255], "layerview_skin": [255, 255, 86, 255], "layerview_support": [255, 255, 0, 255], "layerview_infill": [0, 255, 255, 255], "layerview_support_infill": [0, 200, 200, 255], "layerview_move_retraction": [0, 100, 255, 255], "main_window_header_background": [192, 199, 65, 255]}} \ No newline at end of file diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 377e70f5b6..2362155944 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -1,689 +1 @@ -{ - "metadata": { - "name": "UltiMaker" - }, - - "fonts": { - "large": { - "size": 1.35, - "weight": 400, - "family": "Noto Sans" - }, - "large_ja_JP": { - "size": 1.35, - "weight": 400, - "family": "Noto Sans" - }, - "large_zh_CN": { - "size": 1.35, - "weight": 400, - "family": "Noto Sans" - }, - "large_zh_TW": { - "size": 1.35, - "weight": 400, - "family": "Noto Sans" - }, - "large_bold": { - "size": 1.35, - "weight": 600, - "family": "Noto Sans" - }, - "huge": { - "size": 1.8, - "weight": 400, - "family": "Noto Sans" - }, - "huge_bold": { - "size": 1.8, - "weight": 600, - "family": "Noto Sans" - }, - "medium": { - "size": 1.16, - "weight": 400, - "family": "Noto Sans" - }, - "medium_ja_JP": { - "size": 1.16, - "weight": 400, - "family": "Noto Sans" - }, - "medium_zh_CN": { - "size": 1.16, - "weight": 400, - "family": "Noto Sans" - }, - "medium_zh_TW": { - "size": 1.16, - "weight": 400, - "family": "Noto Sans" - }, - "medium_bold": { - "size": 1.16, - "weight": 600, - "family": "Noto Sans" - }, - "default": { - "size": 0.95, - "weight": 400, - "family": "Noto Sans" - }, - "default_ja_JP": { - "size": 1.0, - "weight": 400, - "family": "Noto Sans" - }, - "default_zh_CN": { - "size": 1.0, - "weight": 400, - "family": "Noto Sans" - }, - "default_zh_TW": { - "size": 1.0, - "weight": 400, - "family": "Noto Sans" - }, - "default_bold": { - "size": 0.95, - "weight": 600, - "family": "Noto Sans" - }, - "default_bold_ja_JP": { - "size": 1.0, - "weight": 600, - "family": "Noto Sans" - }, - "default_bold_zh_CN": { - "size": 1.0, - "weight": 600, - "family": "Noto Sans" - }, - "default_bold_zh_TW": { - "size": 1.0, - "weight": 600, - "family": "Noto Sans" - }, - "default_italic": { - "size": 0.95, - "weight": 400, - "italic": true, - "family": "Noto Sans" - }, - "medium_italic": { - "size": 1.16, - "weight": 400, - "italic": true, - "family": "Noto Sans" - }, - "default_italic_ja_JP": { - "size": 1.0, - "weight": 400, - "italic": true, - "family": "Noto Sans" - }, - "default_italic_zh_CN": { - "size": 1.0, - "weight": 400, - "italic": true, - "family": "Noto Sans" - }, - "default_italic_zh_TW": { - "size": 1.0, - "weight": 400, - "italic": true, - "family": "Noto Sans" - }, - "small": { - "size": 0.9, - "weight": 400, - "family": "Noto Sans" - }, - "small_bold": { - "size": 0.9, - "weight": 700, - "family": "Noto Sans" - }, - "small_ja_JP": { - "size": 0.9, - "weight": 400, - "family": "Noto Sans" - }, - "small_zh_CN": { - "size": 0.9, - "weight": 400, - "family": "Noto Sans" - }, - "small_zh_TW": { - "size": 0.9, - "weight": 400, - "family": "Noto Sans" - }, - "small_emphasis": { - "size": 0.9, - "weight": 700, - "family": "Noto Sans" - }, - "small_emphasis_ja_JP": { - "size": 0.9, - "weight": 700, - "family": "Noto Sans" - }, - "small_emphasis_zh_CN": { - "size": 0.9, - "weight": 700, - "family": "Noto Sans" - }, - "small_emphasis_zh_TW": { - "size": 0.9, - "weight": 700, - "family": "Noto Sans" - }, - "tiny_emphasis": { - "size": 0.7, - "weight": 700, - "family": "Noto Sans" - }, - "tiny_emphasis_ja_JP": { - "size": 0.7, - "weight": 700, - "family": "Noto Sans" - }, - "tiny_emphasis_zh_CN": { - "size": 0.7, - "weight": 700, - "family": "Noto Sans" - }, - "tiny_emphasis_zh_TW": { - "size": 0.7, - "weight": 700, - "family": "Noto Sans" - } - }, - - "base_colors": { - "background_1": [255, 255, 255, 255], - "background_2": [243, 243, 243, 255], - "background_3": [232, 240, 253, 255], - "background_4": [3, 12, 66, 255], - - "accent_1": [25, 110, 240, 255], - "accent_2": [16, 70, 156, 255], - "border_main": [212, 212, 212, 255], - "border_accent_1": [25, 110, 240, 255], - "border_accent_2": [16, 70, 156, 255], - "border_field": [180, 180, 180, 255], - - "text_default": [0, 14, 26, 255], - "text_disabled": [180, 180, 180, 255], - "text_primary_button": [255, 255, 255, 255], - "text_secondary_button": [25, 110, 240, 255], - "text_link_hover": [16, 70, 156, 255], - "text_lighter": [108, 108, 108, 255], - - "um_green_1": [233, 245, 237, 255], - "um_green_5": [36, 162, 73, 255], - "um_green_9": [31, 44, 36, 255], - "um_red_1": [251, 232, 233, 255], - "um_red_5": [218, 30, 40, 255], - "um_red_9": [59, 31, 33, 255], - "um_orange_1": [255, 235, 221, 255], - "um_orange_5": [252, 123, 30, 255], - "um_orange_9": [64, 45, 32, 255], - "um_yellow_1": [255, 248, 225, 255], - "um_yellow_5": [253, 209, 58, 255], - "um_yellow_9": [64, 58, 36, 255] - }, - - "colors": { - - "main_background": "background_1", - "detail_background": "background_2", - "wide_lining": [245, 245, 245, 255], - "thick_lining": [180, 180, 180, 255], - "lining": [192, 193, 194, 255], - "viewport_overlay": [246, 246, 246, 255], - - "primary": "accent_1", - "primary_hover": [48, 182, 231, 255], - "primary_text": [255, 255, 255, 255], - "text_selection": [156, 195, 255, 127], - "border": [127, 127, 127, 255], - "border_field": [180, 180, 180, 255], - "secondary": [240, 240, 240, 255], - - "expandable_active": [240, 240, 240, 255], - "expandable_hover": [232, 242, 252, 255], - - "icon": [8, 7, 63, 255], - - "primary_button": "accent_1", - "primary_button_hover": [16, 70, 156, 255], - "primary_button_text": [255, 255, 255, 255], - - "secondary_button": "background_1", - "secondary_button_shadow": [216, 216, 216, 255], - "secondary_button_hover": [232, 240, 253, 255], - "secondary_button_text": "accent_1", - - "main_window_header_background": [8, 7, 63, 255], - "main_window_header_background_gradient": [25, 23, 91, 255], - "main_window_header_button_text_active": [8, 7, 63, 255], - "main_window_header_button_text_inactive": [255, 255, 255, 255], - "main_window_header_button_text_hovered": [255, 255, 255, 255], - "main_window_header_button_background_active": [255, 255, 255, 255], - "main_window_header_button_background_inactive": [255, 255, 255, 0], - "main_window_header_button_background_hovered": [117, 114, 159, 255], - - "account_widget_outline_active": [70, 66, 126, 255], - "account_sync_state_icon": [25, 25, 25, 255], - - "machine_selector_printer_icon": [8, 7, 63, 255], - - "action_panel_secondary": "accent_1", - - "first_run_shadow": [50, 50, 50, 255], - - "toolbar_background": [255, 255, 255, 255], - - "notification_icon": [255, 0, 0, 255], - - "printer_type_label_background": [228, 228, 242, 255], - - "window_disabled_background": [0, 0, 0, 255], - - "text": [25, 25, 25, 255], - "text_disabled": [180, 180, 180, 255], - "text_detail": [174, 174, 174, 128], - "text_link": "accent_1", - "text_inactive": [174, 174, 174, 255], - "text_medium": [128, 128, 128, 255], - "text_scene": [102, 102, 102, 255], - "text_scene_hover": [123, 123, 113, 255], - - "error": [218, 30, 40, 255], - "warning": [253, 209, 58, 255], - "success": [36, 162, 73, 255], - "disabled": [229, 229, 229, 255], - - "toolbar_button_hover": [232, 242, 252, 255], - "toolbar_button_active": [232, 242, 252, 255], - "toolbar_button_active_hover": [232, 242, 252, 255], - - "button_text": [255, 255, 255, 255], - - "small_button_text": [102, 102, 102, 255], - "small_button_text_hover": [8, 7, 63, 255], - - "button_tooltip": [31, 36, 39, 255], - - "extruder_disabled": [255, 255, 255, 102], - - "action_button": [255, 255, 255, 255], - "action_button_hovered": [232, 242, 252, 255], - "action_button_disabled": [245, 245, 245, 255], - "action_button_disabled_text": [196, 196, 196, 255], - "action_button_shadow": [223, 223, 223, 255], - - "scrollbar_background": [255, 255, 255, 255], - "scrollbar_handle": [10, 8, 80, 255], - "scrollbar_handle_hover": [50, 130, 255, 255], - "scrollbar_handle_down": [50, 130, 255, 255], - - "setting_category": "background_1", - "setting_category_disabled": [255, 255, 255, 255], - "setting_category_hover": "background_2", - "setting_category_text": "text_default", - "setting_category_disabled_text": [24, 41, 77, 101], - "setting_category_active_text": "text_default", - - "setting_control": "background_2", - "setting_control_highlight": "background_3", - "setting_control_border": [199, 199, 199, 255], - "setting_control_border_highlight": [50, 130, 255, 255], - "setting_control_text": [35, 35, 35, 255], - "setting_control_button": [102, 102, 102, 255], - "setting_control_button_hover": [8, 7, 63, 255], - "setting_control_disabled": "background_2", - "setting_control_disabled_text": [127, 127, 127, 255], - "setting_control_disabled_border": [127, 127, 127, 255], - "setting_unit": [127, 127, 127, 255], - "setting_validation_error_background": "um_red_1", - "setting_validation_error": "um_red_5", - "setting_validation_warning_background": "um_yellow_1", - "setting_validation_warning": "um_yellow_5", - "setting_validation_ok": "background_2", - - "material_compatibility_warning": [243, 166, 59, 255], - "core_compatibility_warning": [243, 166, 59, 255], - - "progressbar_background": [245, 245, 245, 255], - "progressbar_control": [50, 130, 255, 255], - - "slider_groove": [223, 223, 223, 255], - "slider_groove_fill": [8, 7, 63, 255], - "slider_handle": [8, 7, 63, 255], - "slider_handle_active": [50, 130, 255, 255], - "slider_text_background": [255, 255, 255, 255], - - "quality_slider_unavailable": [179, 179, 179, 255], - "quality_slider_available": [0, 0, 0, 255], - - "checkbox": "background_1", - "checkbox_hover": "background_1", - "checkbox_disabled": "background_2", - "checkbox_border": [180, 180, 180, 255], - "checkbox_border_hover": "border_main", - "checkbox_border_disabled": "text_disabled", - "checkbox_mark": "text_default", - "checkbox_mark_disabled": "text_disabled", - "checkbox_square": [180, 180, 180, 255], - "checkbox_text": "text_default", - "checkbox_text_disabled": "text_disabled", - - "switch": "background_1", - "switch_state_checked": "accent_1", - "switch_state_unchecked": "text_disabled", - - "radio": "background_1", - "radio_disabled": "background_2", - "radio_selected": "accent_1", - "radio_selected_disabled": "text_disabled", - "radio_border": [180, 180, 180, 255], - "radio_border_hover": "border_main", - "radio_border_disabled": "text_disabled", - "radio_dot": "background_1", - "radio_dot_disabled": "background_2", - "radio_text": "text_default", - "radio_text_disabled": "text_disabled", - - "text_field": "background_1", - "text_field_border": [180, 180, 180, 255], - "text_field_border_hovered": "border_main", - "text_field_border_active": "border_accent_2", - "text_field_border_disabled": "background_2", - "text_field_text": "text_default", - "text_field_text_disabled": "text_disabled", - - "category_background": "background_2", - - "tooltip": [25, 25, 25, 255], - "tooltip_text": [255, 255, 255, 255], - - "message_background": [255, 255, 255, 255], - "message_border": [192, 193, 194, 255], - "message_close": [102, 102, 102, 255], - "message_close_hover": [8, 7, 63, 255], - "message_progressbar_background": [245, 245, 245, 255], - "message_progressbar_control": [50, 130, 255, 255], - "message_success_icon": [255, 255, 255, 255], - "message_warning_icon": [0, 0, 0, 255], - "message_error_icon": [255, 255, 255, 255], - - "tool_panel_background": [255, 255, 255, 255], - - "status_offline": [0, 0, 0, 255], - "status_ready": [0, 205, 0, 255], - "status_busy": [50, 130, 255, 255], - "status_paused": [255, 140, 0, 255], - "status_stopped": [236, 82, 80, 255], - - "disabled_axis": [127, 127, 127, 255], - "x_axis": [218, 30, 40, 255], - "y_axis": [25, 110, 240, 255], - "z_axis": [36, 162, 73, 255], - "all_axis": [255, 255, 255, 255], - - "viewport_background": [250, 250, 250, 255], - "volume_outline": [50, 130, 255, 255], - "buildplate": [244, 244, 244, 255], - "buildplate_grid": [180, 180, 180, 255], - "buildplate_grid_minor": [228, 228, 228, 255], - - "convex_hull": [35, 35, 35, 127], - "disallowed_area": [0, 0, 0, 40], - "error_area": [255, 0, 0, 127], - - "model_overhang": [255, 0, 0, 255], - "model_unslicable": [122, 122, 122, 255], - "model_unslicable_alt": [172, 172, 127, 255], - "model_selection_outline": [50, 130, 255, 255], - "model_non_printing": [122, 122, 122, 255], - - "xray": [26, 26, 62, 255], - - "layerview_ghost": [31, 31, 31, 95], - "layerview_none": [255, 255, 255, 255], - "layerview_inset_0": [230, 0, 0, 255], - "layerview_inset_x": [0, 230, 0, 255], - "layerview_skin": [230, 230, 0, 255], - "layerview_support": [0, 230, 230, 127], - "layerview_skirt": [0, 230, 230, 255], - "layerview_infill": [230, 115, 0, 255], - "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], - "layerview_starts": [255, 255, 255, 255], - - - "monitor_printer_family_tag": [228, 228, 242, 255], - "monitor_text_disabled": [238, 238, 238, 255], - "monitor_icon_primary": [10, 8, 80, 255], - "monitor_icon_accent": [255, 255, 255, 255], - "monitor_icon_disabled": [238, 238, 238, 255], - - "monitor_card_border": [192, 193, 194, 255], - "monitor_card_background": [255, 255, 255, 255], - "monitor_card_hover": [232, 242, 252, 255], - - "monitor_stage_background": [246, 246, 246, 255], - "monitor_stage_background_fade": [246, 246, 246, 102], - - "monitor_tooltip": [25, 25, 25, 255], - "monitor_tooltip_text": [255, 255, 255, 255], - "monitor_context_menu": [255, 255, 255, 255], - "monitor_context_menu_hover": [245, 245, 245, 255], - - "monitor_skeleton_loading": [238, 238, 238, 255], - "monitor_placeholder_image": [230, 230, 230, 255], - "monitor_image_overlay": [0, 0, 0, 255], - "monitor_shadow": [200, 200, 200, 255], - - "monitor_carousel_dot": [216, 216, 216, 255], - "monitor_carousel_dot_current": [119, 119, 119, 255], - - "cloud_unavailable": [153, 153, 153, 255], - "connection_badge_background": [255, 255, 255, 255], - "warning_badge_background": [0, 0, 0, 255], - "error_badge_background": [255, 255, 255, 255], - - "border_field_light": [180, 180, 180, 255], - "border_main_light": [212, 212, 212, 255] - }, - - "sizes": { - "window_minimum_size": [80, 48], - "popup_dialog": [40, 36], - "small_popup_dialog": [36, 12], - - "main_window_header": [0.0, 4.0], - - "stage_menu": [0.0, 4.0], - - "account_button": [12, 2.5], - - "print_setup_widget": [38.0, 30.0], - "print_setup_extruder_box": [0.0, 6.0], - "slider_widget_groove": [0.16, 0.16], - "slider_widget_handle": [1.3, 1.3], - "slider_widget_tickmarks": [0.5, 0.5], - "print_setup_big_item": [28, 2.5], - "print_setup_icon": [1.2, 1.2], - "drag_icon": [1.416, 0.25], - - "application_switcher_item": [8, 9], - "application_switcher_icon": [3.75, 3.75], - - "expandable_component_content_header": [0.0, 3.0], - - "configuration_selector": [35.0, 4.0], - - "action_panel_widget": [26.0, 0.0], - "action_panel_information_widget": [20.0, 0.0], - - "machine_selector_widget": [20.0, 4.0], - "machine_selector_widget_content": [25.0, 32.0], - "machine_selector_icon": [2.5, 2.5], - - "views_selector": [16.0, 4.0], - - "printer_type_label": [3.5, 1.5], - - "default_radius": [0.25, 0.25], - - "wide_lining": [0.5, 0.5], - "thick_lining": [0.2, 0.2], - "default_lining": [0.08, 0.08], - - "default_arrow": [0.8, 0.8], - "logo": [16, 2], - - "wide_margin": [2.0, 2.0], - "thick_margin": [1.71, 1.43], - "default_margin": [1.0, 1.0], - "thin_margin": [0.71, 0.71], - "narrow_margin": [0.5, 0.5], - - "extruder_icon": [2.5, 2.5], - - "section": [0.0, 2], - "section_header": [0.0, 2.5], - - "section_control": [0, 1], - "section_icon": [1.5, 1.5], - "section_icon_column": [2.5, 2.5], - - "setting": [25.0, 1.8], - "setting_control": [9.0, 2.0], - "setting_control_radius": [0.15, 0.15], - "setting_control_depth_margin": [1.4, 0.0], - "setting_unit_margin": [0.5, 0.5], - - "standard_list_lineheight": [1.5, 1.5], - "standard_arrow": [1.0, 1.0], - - "card": [25.0, 10], - "card_icon": [6.0, 6.0], - "card_tiny_icon": [1.5, 1.5], - - "button": [4, 4], - "button_icon": [2.5, 2.5], - - "action_button": [15.0, 2.5], - "action_button_icon": [1.5, 1.5], - "action_button_icon_small": [1.0, 1.0], - "action_button_radius": [0.15, 0.15], - - "radio_button": [1.3, 1.3], - - "small_button": [2, 2], - "small_button_icon": [1.5, 1.5], - - "medium_button": [2.5, 2.5], - "medium_button_icon": [2, 2], - - "large_button": [3.0, 3.0], - "large_button_icon": [2.8, 2.8], - - "context_menu": [20, 2], - - "icon_indicator": [1, 1], - - "printer_status_icon": [1.0, 1.0], - - "button_tooltip": [1.0, 1.3], - "button_tooltip_arrow": [0.25, 0.25], - - "progressbar": [26.0, 0.75], - "progressbar_radius": [0.15, 0.15], - - "scrollbar": [0.75, 0.5], - - "slider_groove": [0.5, 0.5], - "slider_groove_radius": [0.15, 0.15], - "slider_handle": [1.5, 1.5], - "slider_layerview_size": [1.0, 34.0], - - "layerview_menu_size": [16.0, 4.0], - "layerview_legend_size": [1.0, 1.0], - "layerview_row": [11.0, 1.5], - "layerview_row_spacing": [0.0, 0.5], - - "checkbox": [1.33, 1.33], - "checkbox_mark": [1, 1], - "checkbox_radius": [0.25, 0.25], - - "spinbox": [6.0, 3.0], - "combobox": [14, 2], - "combobox_wide": [22, 2], - - "tooltip": [20.0, 10.0], - "tooltip_margins": [1.0, 1.0], - "tooltip_arrow_margins": [2.0, 2.0], - - "save_button_save_to_button": [0.3, 2.7], - "save_button_specs_icons": [1.4, 1.4], - - "first_run_shadow_radius": [1.2, 1.2], - - "monitor_preheat_temperature_control": [4.5, 2.0], - - "welcome_wizard_window": [46, 50], - "modal_window_minimum": [60.0, 50.0], - "wizard_progress": [10.0, 0.0], - - "message": [30.0, 5.0], - "message_close": [2, 2], - "message_radius": [0.25, 0.25], - "message_action_button": [0, 2.5], - "message_image": [15.0, 10.0], - "message_type_icon": [2, 2], - "menu": [18, 2], - - "jobspecs_line": [2.0, 2.0], - - "objects_menu_size": [15, 15], - - "notification_icon": [1.5, 1.5], - - "avatar_image": [6.8, 6.8], - - "monitor_shadow_radius": [0.4, 0.4], - "monitor_empty_state_offset": [5.6, 5.6], - "monitor_empty_state_size": [35.0, 25.0], - "monitor_column": [18.0, 1.0], - "monitor_progress_bar": [16.5, 1.0], - - "table_row": [2.0, 2.0], - - "welcome_wizard_content_image_big": [18, 15], - "welcome_wizard_cloud_content_image": [4, 4], - - "banner_icon_size": [2.0, 2.0], - - "marketplace_large_icon": [4.0, 4.0], - - "preferences_page_list_item": [8.0, 2.0], - - "recommended_button_icon": [1.7, 1.7], - - "recommended_section_setting_item": [14.0, 2.0], - - "reset_profile_icon": [1, 1] - } -} +{"metadata": {"name": "UltiMaker"}, "fonts": {"large": {"size": 1.35, "weight": 400, "family": "Noto Sans"}, "large_ja_JP": {"size": 1.35, "weight": 400, "family": "Noto Sans"}, "large_zh_CN": {"size": 1.35, "weight": 400, "family": "Noto Sans"}, "large_zh_TW": {"size": 1.35, "weight": 400, "family": "Noto Sans"}, "large_bold": {"size": 1.35, "weight": 600, "family": "Noto Sans"}, "huge": {"size": 1.8, "weight": 400, "family": "Noto Sans"}, "huge_bold": {"size": 1.8, "weight": 600, "family": "Noto Sans"}, "medium": {"size": 1.16, "weight": 400, "family": "Noto Sans"}, "medium_ja_JP": {"size": 1.16, "weight": 400, "family": "Noto Sans"}, "medium_zh_CN": {"size": 1.16, "weight": 400, "family": "Noto Sans"}, "medium_zh_TW": {"size": 1.16, "weight": 400, "family": "Noto Sans"}, "medium_bold": {"size": 1.16, "weight": 600, "family": "Noto Sans"}, "default": {"size": 0.95, "weight": 400, "family": "Noto Sans"}, "default_ja_JP": {"size": 1.0, "weight": 400, "family": "Noto Sans"}, "default_zh_CN": {"size": 1.0, "weight": 400, "family": "Noto Sans"}, "default_zh_TW": {"size": 1.0, "weight": 400, "family": "Noto Sans"}, "default_bold": {"size": 0.95, "weight": 600, "family": "Noto Sans"}, "default_bold_ja_JP": {"size": 1.0, "weight": 600, "family": "Noto Sans"}, "default_bold_zh_CN": {"size": 1.0, "weight": 600, "family": "Noto Sans"}, "default_bold_zh_TW": {"size": 1.0, "weight": 600, "family": "Noto Sans"}, "default_italic": {"size": 0.95, "weight": 400, "italic": true, "family": "Noto Sans"}, "medium_italic": {"size": 1.16, "weight": 400, "italic": true, "family": "Noto Sans"}, "default_italic_ja_JP": {"size": 1.0, "weight": 400, "italic": true, "family": "Noto Sans"}, "default_italic_zh_CN": {"size": 1.0, "weight": 400, "italic": true, "family": "Noto Sans"}, "default_italic_zh_TW": {"size": 1.0, "weight": 400, "italic": true, "family": "Noto Sans"}, "small": {"size": 0.9, "weight": 400, "family": "Noto Sans"}, "small_bold": {"size": 0.9, "weight": 700, "family": "Noto Sans"}, "small_ja_JP": {"size": 0.9, "weight": 400, "family": "Noto Sans"}, "small_zh_CN": {"size": 0.9, "weight": 400, "family": "Noto Sans"}, "small_zh_TW": {"size": 0.9, "weight": 400, "family": "Noto Sans"}, "small_emphasis": {"size": 0.9, "weight": 700, "family": "Noto Sans"}, "small_emphasis_ja_JP": {"size": 0.9, "weight": 700, "family": "Noto Sans"}, "small_emphasis_zh_CN": {"size": 0.9, "weight": 700, "family": "Noto Sans"}, "small_emphasis_zh_TW": {"size": 0.9, "weight": 700, "family": "Noto Sans"}, "tiny_emphasis": {"size": 0.7, "weight": 700, "family": "Noto Sans"}, "tiny_emphasis_ja_JP": {"size": 0.7, "weight": 700, "family": "Noto Sans"}, "tiny_emphasis_zh_CN": {"size": 0.7, "weight": 700, "family": "Noto Sans"}, "tiny_emphasis_zh_TW": {"size": 0.7, "weight": 700, "family": "Noto Sans"}}, "base_colors": {"background_1": [255, 255, 255, 255], "background_2": [243, 243, 243, 255], "background_3": [232, 240, 253, 255], "background_4": [3, 12, 66, 255], "accent_1": [25, 110, 240, 255], "accent_2": [16, 70, 156, 255], "border_main": [212, 212, 212, 255], "border_accent_1": [25, 110, 240, 255], "border_accent_2": [16, 70, 156, 255], "border_field": [180, 180, 180, 255], "text_default": [0, 14, 26, 255], "text_disabled": [180, 180, 180, 255], "text_primary_button": [255, 255, 255, 255], "text_secondary_button": [25, 110, 240, 255], "text_link_hover": [16, 70, 156, 255], "text_lighter": [108, 108, 108, 255], "um_green_1": [233, 245, 237, 255], "um_green_5": [36, 162, 73, 255], "um_green_9": [31, 44, 36, 255], "um_red_1": [251, 232, 233, 255], "um_red_5": [218, 30, 40, 255], "um_red_9": [59, 31, 33, 255], "um_orange_1": [255, 235, 221, 255], "um_orange_5": [252, 123, 30, 255], "um_orange_9": [64, 45, 32, 255], "um_yellow_1": [255, 248, 225, 255], "um_yellow_5": [253, 209, 58, 255], "um_yellow_9": [64, 58, 36, 255]}, "colors": {"main_background": "background_1", "detail_background": "background_2", "wide_lining": [245, 245, 245, 255], "thick_lining": [180, 180, 180, 255], "lining": [192, 193, 194, 255], "viewport_overlay": [246, 246, 246, 255], "primary": "accent_1", "primary_hover": [48, 182, 231, 255], "primary_text": [255, 255, 255, 255], "text_selection": [156, 195, 255, 127], "border": [127, 127, 127, 255], "border_field": [180, 180, 180, 255], "secondary": [240, 240, 240, 255], "expandable_active": [240, 240, 240, 255], "expandable_hover": [232, 242, 252, 255], "icon": [8, 7, 63, 255], "primary_button": "accent_1", "primary_button_hover": [16, 70, 156, 255], "primary_button_text": [255, 255, 255, 255], "secondary_button": "background_1", "secondary_button_shadow": [216, 216, 216, 255], "secondary_button_hover": [232, 240, 253, 255], "secondary_button_text": "accent_1", "main_window_header_background": [192, 199, 65, 255], "main_window_header_background_gradient": [25, 23, 91, 255], "main_window_header_button_text_active": [8, 7, 63, 255], "main_window_header_button_text_inactive": [255, 255, 255, 255], "main_window_header_button_text_hovered": [255, 255, 255, 255], "main_window_header_button_background_active": [255, 255, 255, 255], "main_window_header_button_background_inactive": [255, 255, 255, 0], "main_window_header_button_background_hovered": [117, 114, 159, 255], "account_widget_outline_active": [70, 66, 126, 255], "account_sync_state_icon": [25, 25, 25, 255], "machine_selector_printer_icon": [8, 7, 63, 255], "action_panel_secondary": "accent_1", "first_run_shadow": [50, 50, 50, 255], "toolbar_background": [255, 255, 255, 255], "notification_icon": [255, 0, 0, 255], "printer_type_label_background": [228, 228, 242, 255], "window_disabled_background": [0, 0, 0, 255], "text": [25, 25, 25, 255], "text_disabled": [180, 180, 180, 255], "text_detail": [174, 174, 174, 128], "text_link": "accent_1", "text_inactive": [174, 174, 174, 255], "text_medium": [128, 128, 128, 255], "text_scene": [102, 102, 102, 255], "text_scene_hover": [123, 123, 113, 255], "error": [218, 30, 40, 255], "warning": [253, 209, 58, 255], "success": [36, 162, 73, 255], "disabled": [229, 229, 229, 255], "toolbar_button_hover": [232, 242, 252, 255], "toolbar_button_active": [232, 242, 252, 255], "toolbar_button_active_hover": [232, 242, 252, 255], "button_text": [255, 255, 255, 255], "small_button_text": [102, 102, 102, 255], "small_button_text_hover": [8, 7, 63, 255], "button_tooltip": [31, 36, 39, 255], "extruder_disabled": [255, 255, 255, 102], "action_button": [255, 255, 255, 255], "action_button_hovered": [232, 242, 252, 255], "action_button_disabled": [245, 245, 245, 255], "action_button_disabled_text": [196, 196, 196, 255], "action_button_shadow": [223, 223, 223, 255], "scrollbar_background": [255, 255, 255, 255], "scrollbar_handle": [10, 8, 80, 255], "scrollbar_handle_hover": [50, 130, 255, 255], "scrollbar_handle_down": [50, 130, 255, 255], "setting_category": "background_1", "setting_category_disabled": [255, 255, 255, 255], "setting_category_hover": "background_2", "setting_category_text": "text_default", "setting_category_disabled_text": [24, 41, 77, 101], "setting_category_active_text": "text_default", "setting_control": "background_2", "setting_control_highlight": "background_3", "setting_control_border": [199, 199, 199, 255], "setting_control_border_highlight": [50, 130, 255, 255], "setting_control_text": [35, 35, 35, 255], "setting_control_button": [102, 102, 102, 255], "setting_control_button_hover": [8, 7, 63, 255], "setting_control_disabled": "background_2", "setting_control_disabled_text": [127, 127, 127, 255], "setting_control_disabled_border": [127, 127, 127, 255], "setting_unit": [127, 127, 127, 255], "setting_validation_error_background": "um_red_1", "setting_validation_error": "um_red_5", "setting_validation_warning_background": "um_yellow_1", "setting_validation_warning": "um_yellow_5", "setting_validation_ok": "background_2", "material_compatibility_warning": [243, 166, 59, 255], "core_compatibility_warning": [243, 166, 59, 255], "progressbar_background": [245, 245, 245, 255], "progressbar_control": [50, 130, 255, 255], "slider_groove": [223, 223, 223, 255], "slider_groove_fill": [8, 7, 63, 255], "slider_handle": [8, 7, 63, 255], "slider_handle_active": [50, 130, 255, 255], "slider_text_background": [255, 255, 255, 255], "quality_slider_unavailable": [179, 179, 179, 255], "quality_slider_available": [0, 0, 0, 255], "checkbox": "background_1", "checkbox_hover": "background_1", "checkbox_disabled": "background_2", "checkbox_border": [180, 180, 180, 255], "checkbox_border_hover": "border_main", "checkbox_border_disabled": "text_disabled", "checkbox_mark": "text_default", "checkbox_mark_disabled": "text_disabled", "checkbox_square": [180, 180, 180, 255], "checkbox_text": "text_default", "checkbox_text_disabled": "text_disabled", "switch": "background_1", "switch_state_checked": "accent_1", "switch_state_unchecked": "text_disabled", "radio": "background_1", "radio_disabled": "background_2", "radio_selected": "accent_1", "radio_selected_disabled": "text_disabled", "radio_border": [180, 180, 180, 255], "radio_border_hover": "border_main", "radio_border_disabled": "text_disabled", "radio_dot": "background_1", "radio_dot_disabled": "background_2", "radio_text": "text_default", "radio_text_disabled": "text_disabled", "text_field": "background_1", "text_field_border": [180, 180, 180, 255], "text_field_border_hovered": "border_main", "text_field_border_active": "border_accent_2", "text_field_border_disabled": "background_2", "text_field_text": "text_default", "text_field_text_disabled": "text_disabled", "category_background": "background_2", "tooltip": [25, 25, 25, 255], "tooltip_text": [255, 255, 255, 255], "message_background": [255, 255, 255, 255], "message_border": [192, 193, 194, 255], "message_close": [102, 102, 102, 255], "message_close_hover": [8, 7, 63, 255], "message_progressbar_background": [245, 245, 245, 255], "message_progressbar_control": [50, 130, 255, 255], "message_success_icon": [255, 255, 255, 255], "message_warning_icon": [0, 0, 0, 255], "message_error_icon": [255, 255, 255, 255], "tool_panel_background": [255, 255, 255, 255], "status_offline": [0, 0, 0, 255], "status_ready": [0, 205, 0, 255], "status_busy": [50, 130, 255, 255], "status_paused": [255, 140, 0, 255], "status_stopped": [236, 82, 80, 255], "disabled_axis": [127, 127, 127, 255], "x_axis": [218, 30, 40, 255], "y_axis": [25, 110, 240, 255], "z_axis": [36, 162, 73, 255], "all_axis": [255, 255, 255, 255], "viewport_background": [250, 250, 250, 255], "volume_outline": [50, 130, 255, 255], "buildplate": [244, 244, 244, 255], "buildplate_grid": [180, 180, 180, 255], "buildplate_grid_minor": [228, 228, 228, 255], "convex_hull": [35, 35, 35, 127], "disallowed_area": [0, 0, 0, 40], "error_area": [255, 0, 0, 127], "model_overhang": [255, 0, 0, 255], "model_unslicable": [122, 122, 122, 255], "model_unslicable_alt": [172, 172, 127, 255], "model_selection_outline": [50, 130, 255, 255], "model_non_printing": [122, 122, 122, 255], "xray": [26, 26, 62, 255], "layerview_ghost": [31, 31, 31, 95], "layerview_none": [255, 255, 255, 255], "layerview_inset_0": [230, 0, 0, 255], "layerview_inset_x": [0, 230, 0, 255], "layerview_skin": [230, 230, 0, 255], "layerview_support": [0, 230, 230, 127], "layerview_skirt": [0, 230, 230, 255], "layerview_infill": [230, 115, 0, 255], "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], "layerview_starts": [255, 255, 255, 255], "monitor_printer_family_tag": [228, 228, 242, 255], "monitor_text_disabled": [238, 238, 238, 255], "monitor_icon_primary": [10, 8, 80, 255], "monitor_icon_accent": [255, 255, 255, 255], "monitor_icon_disabled": [238, 238, 238, 255], "monitor_card_border": [192, 193, 194, 255], "monitor_card_background": [255, 255, 255, 255], "monitor_card_hover": [232, 242, 252, 255], "monitor_stage_background": [246, 246, 246, 255], "monitor_stage_background_fade": [246, 246, 246, 102], "monitor_tooltip": [25, 25, 25, 255], "monitor_tooltip_text": [255, 255, 255, 255], "monitor_context_menu": [255, 255, 255, 255], "monitor_context_menu_hover": [245, 245, 245, 255], "monitor_skeleton_loading": [238, 238, 238, 255], "monitor_placeholder_image": [230, 230, 230, 255], "monitor_image_overlay": [0, 0, 0, 255], "monitor_shadow": [200, 200, 200, 255], "monitor_carousel_dot": [216, 216, 216, 255], "monitor_carousel_dot_current": [119, 119, 119, 255], "cloud_unavailable": [153, 153, 153, 255], "connection_badge_background": [255, 255, 255, 255], "warning_badge_background": [0, 0, 0, 255], "error_badge_background": [255, 255, 255, 255], "border_field_light": [180, 180, 180, 255], "border_main_light": [212, 212, 212, 255]}, "sizes": {"window_minimum_size": [80, 48], "popup_dialog": [40, 36], "small_popup_dialog": [36, 12], "main_window_header": [0.0, 4.0], "stage_menu": [0.0, 4.0], "account_button": [12, 2.5], "print_setup_widget": [38.0, 30.0], "print_setup_extruder_box": [0.0, 6.0], "slider_widget_groove": [0.16, 0.16], "slider_widget_handle": [1.3, 1.3], "slider_widget_tickmarks": [0.5, 0.5], "print_setup_big_item": [28, 2.5], "print_setup_icon": [1.2, 1.2], "drag_icon": [1.416, 0.25], "application_switcher_item": [8, 9], "application_switcher_icon": [3.75, 3.75], "expandable_component_content_header": [0.0, 3.0], "configuration_selector": [35.0, 4.0], "action_panel_widget": [26.0, 0.0], "action_panel_information_widget": [20.0, 0.0], "machine_selector_widget": [20.0, 4.0], "machine_selector_widget_content": [25.0, 32.0], "machine_selector_icon": [2.5, 2.5], "views_selector": [16.0, 4.0], "printer_type_label": [3.5, 1.5], "default_radius": [0.25, 0.25], "wide_lining": [0.5, 0.5], "thick_lining": [0.2, 0.2], "default_lining": [0.08, 0.08], "default_arrow": [0.8, 0.8], "logo": [16, 2], "wide_margin": [2.0, 2.0], "thick_margin": [1.71, 1.43], "default_margin": [1.0, 1.0], "thin_margin": [0.71, 0.71], "narrow_margin": [0.5, 0.5], "extruder_icon": [2.5, 2.5], "section": [0.0, 2], "section_header": [0.0, 2.5], "section_control": [0, 1], "section_icon": [1.5, 1.5], "section_icon_column": [2.5, 2.5], "setting": [25.0, 1.8], "setting_control": [9.0, 2.0], "setting_control_radius": [0.15, 0.15], "setting_control_depth_margin": [1.4, 0.0], "setting_unit_margin": [0.5, 0.5], "standard_list_lineheight": [1.5, 1.5], "standard_arrow": [1.0, 1.0], "card": [25.0, 10], "card_icon": [6.0, 6.0], "card_tiny_icon": [1.5, 1.5], "button": [4, 4], "button_icon": [2.5, 2.5], "action_button": [15.0, 2.5], "action_button_icon": [1.5, 1.5], "action_button_icon_small": [1.0, 1.0], "action_button_radius": [0.15, 0.15], "radio_button": [1.3, 1.3], "small_button": [2, 2], "small_button_icon": [1.5, 1.5], "medium_button": [2.5, 2.5], "medium_button_icon": [2, 2], "large_button": [3.0, 3.0], "large_button_icon": [2.8, 2.8], "context_menu": [20, 2], "icon_indicator": [1, 1], "printer_status_icon": [1.0, 1.0], "button_tooltip": [1.0, 1.3], "button_tooltip_arrow": [0.25, 0.25], "progressbar": [26.0, 0.75], "progressbar_radius": [0.15, 0.15], "scrollbar": [0.75, 0.5], "slider_groove": [0.5, 0.5], "slider_groove_radius": [0.15, 0.15], "slider_handle": [1.5, 1.5], "slider_layerview_size": [1.0, 34.0], "layerview_menu_size": [16.0, 4.0], "layerview_legend_size": [1.0, 1.0], "layerview_row": [11.0, 1.5], "layerview_row_spacing": [0.0, 0.5], "checkbox": [1.33, 1.33], "checkbox_mark": [1, 1], "checkbox_radius": [0.25, 0.25], "spinbox": [6.0, 3.0], "combobox": [14, 2], "combobox_wide": [22, 2], "tooltip": [20.0, 10.0], "tooltip_margins": [1.0, 1.0], "tooltip_arrow_margins": [2.0, 2.0], "save_button_save_to_button": [0.3, 2.7], "save_button_specs_icons": [1.4, 1.4], "first_run_shadow_radius": [1.2, 1.2], "monitor_preheat_temperature_control": [4.5, 2.0], "welcome_wizard_window": [46, 50], "modal_window_minimum": [60.0, 50.0], "wizard_progress": [10.0, 0.0], "message": [30.0, 5.0], "message_close": [2, 2], "message_radius": [0.25, 0.25], "message_action_button": [0, 2.5], "message_image": [15.0, 10.0], "message_type_icon": [2, 2], "menu": [18, 2], "jobspecs_line": [2.0, 2.0], "objects_menu_size": [15, 15], "notification_icon": [1.5, 1.5], "avatar_image": [6.8, 6.8], "monitor_shadow_radius": [0.4, 0.4], "monitor_empty_state_offset": [5.6, 5.6], "monitor_empty_state_size": [35.0, 25.0], "monitor_column": [18.0, 1.0], "monitor_progress_bar": [16.5, 1.0], "table_row": [2.0, 2.0], "welcome_wizard_content_image_big": [18, 15], "welcome_wizard_cloud_content_image": [4, 4], "banner_icon_size": [2.0, 2.0], "marketplace_large_icon": [4.0, 4.0], "preferences_page_list_item": [8.0, 2.0], "recommended_button_icon": [1.7, 1.7], "recommended_section_setting_item": [14.0, 2.0], "reset_profile_icon": [1, 1]}} \ No newline at end of file diff --git a/resources/themes/daily_test_colors.json b/resources/themes/daily_test_colors.json deleted file mode 100644 index 1cfa2baa74..0000000000 --- a/resources/themes/daily_test_colors.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - [ 62, 33, 55, 255], - [126, 196, 193, 255], - [126, 196, 193, 255], - [215, 155, 125, 255], - [228, 148, 58, 255], - [192, 199, 65, 255], - [157, 48, 59, 255], - [140, 143, 174, 255], - [ 23, 67, 75, 255], - [ 23, 67, 75, 255], - [154, 99, 72, 255], - [112, 55, 127, 255], - [100, 125, 52, 255], - [210, 100, 113, 255] -] From 57f811af8818cea54fd1bf720c147ec4bdfcf006 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 3 Jun 2025 15:59:02 +0200 Subject: [PATCH 024/159] Load painted texture from 3MF file CURA-12544 --- cura/Scene/SliceableObjectDecorator.py | 11 ++++++++++- plugins/3MFReader/ThreeMFReader.py | 13 ++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/cura/Scene/SliceableObjectDecorator.py b/cura/Scene/SliceableObjectDecorator.py index a52f7badf4..bb15173c01 100644 --- a/cura/Scene/SliceableObjectDecorator.py +++ b/cura/Scene/SliceableObjectDecorator.py @@ -1,5 +1,11 @@ +import copy + +from typing import Optional + +import UM.View.GL.Texture from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from UM.View.GL.OpenGL import OpenGL +from UM.View.GL.Texture import Texture # FIXME: When the texture UV-unwrapping is done, these two values will need to be set to a proper value (suggest 4096 for both). @@ -14,10 +20,13 @@ class SliceableObjectDecorator(SceneNodeDecorator): def isSliceable(self) -> bool: return True - def getPaintTexture(self, create_if_required: bool = True): + def getPaintTexture(self, create_if_required: bool = True) -> Optional[UM.View.GL.Texture.Texture]: if self._paint_texture is None and create_if_required: self._paint_texture = OpenGL.getInstance().createTexture(TEXTURE_WIDTH, TEXTURE_HEIGHT) return self._paint_texture + def setPaintTexture(self, texture: UM.View.GL.Texture) -> None: + self._paint_texture = texture + def __deepcopy__(self, memo) -> "SliceableObjectDecorator": return type(self)() diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index 5c8d803f3b..4275fbc7b5 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -7,6 +7,7 @@ from typing import List, Optional, Union, TYPE_CHECKING, cast import pySavitar as Savitar import numpy +from PyQt6.QtGui import QImage from UM.Logger import Logger from UM.Math.Matrix import Matrix @@ -18,6 +19,8 @@ from UM.Scene.GroupDecorator import GroupDecorator from UM.Scene.SceneNode import SceneNode # For typing. from UM.Scene.SceneNodeSettings import SceneNodeSettings from UM.Util import parseBool +from UM.View.GL.OpenGL import OpenGL +from UM.View.GL.Texture import Texture from cura.CuraApplication import CuraApplication from cura.Machines.ContainerTree import ContainerTree from cura.Scene.BuildPlateDecorator import BuildPlateDecorator @@ -101,7 +104,7 @@ class ThreeMFReader(MeshReader): """ try: node_name = savitar_node.getName() - node_id = savitar_node.getId() + node_id = str(savitar_node.getId()) except AttributeError: Logger.log("e", "Outdated version of libSavitar detected! Please update to the newest version!") node_name = "" @@ -226,6 +229,14 @@ class ThreeMFReader(MeshReader): # affects (auto) slicing sliceable_decorator = SliceableObjectDecorator() um_node.addDecorator(sliceable_decorator) + + if texture_path != "" and archive is not None: + texture_data = archive.open(texture_path).read() + texture_image = QImage.fromData(texture_data, "PNG") + texture = Texture(OpenGL.getInstance()) + texture.setImage(texture_image) + sliceable_decorator.setPaintTexture(texture) + return um_node def _read(self, file_name: str) -> Union[SceneNode, List[SceneNode]]: From 5a4b5bf11992f0e55dd7e8bf9cc7b1ca9708e4c7 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 3 Jun 2025 15:59:44 +0200 Subject: [PATCH 025/159] Allow properly duplicating painted models CURA-12544 --- cura/Scene/SliceableObjectDecorator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cura/Scene/SliceableObjectDecorator.py b/cura/Scene/SliceableObjectDecorator.py index bb15173c01..c26848ed1a 100644 --- a/cura/Scene/SliceableObjectDecorator.py +++ b/cura/Scene/SliceableObjectDecorator.py @@ -29,4 +29,6 @@ class SliceableObjectDecorator(SceneNodeDecorator): self._paint_texture = texture def __deepcopy__(self, memo) -> "SliceableObjectDecorator": - return type(self)() + copied_decorator = SliceableObjectDecorator() + copied_decorator.setPaintTexture(copy.deepcopy(self.getPaintTexture())) + return copied_decorator From b940179c54f224dfa6420a65a8f91f615f7478b6 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 4 Jun 2025 11:01:51 +0200 Subject: [PATCH 026/159] Set proper tooltip text color CURA-11978 --- plugins/SimulationView/SimulationViewMenuComponent.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/SimulationView/SimulationViewMenuComponent.qml b/plugins/SimulationView/SimulationViewMenuComponent.qml index dcd2c7d178..0e254a005b 100644 --- a/plugins/SimulationView/SimulationViewMenuComponent.qml +++ b/plugins/SimulationView/SimulationViewMenuComponent.qml @@ -339,7 +339,7 @@ Cura.ExpandableComponent height: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height width: UM.Theme.getSize("layerview_menu_size").width - color: UM.Theme.getColor("setting_control_text") + color: UM.Theme.getColor("tooltip_text") Rectangle { anchors.verticalCenter: parent.verticalCenter From 12d788db62facc68334820216b9a38f70806cb80 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 4 Jun 2025 21:00:27 +0200 Subject: [PATCH 027/159] Review comments: Fix crash when click next to object. Refactoring that part to up top caused the problem I think -- getSelectedObject(0) over getAllSelectedObjects()[0] is clearly the better call in this case anyway. part of CURA-12543 --- plugins/PaintTool/PaintTool.py | 11 +++++------ plugins/PaintTool/PaintView.py | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index 2f6d3736f3..06c066e725 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -111,9 +111,9 @@ class PaintTool(Tool): paintview.redoStroke() else: paintview.undoStroke() - nodes = Selection.getAllSelectedObjects() - if len(nodes) > 0: - Application.getInstance().getController().getScene().sceneChanged.emit(nodes[0]) + node = Selection.getSelectedObject(0) + if node is not None: + Application.getInstance().getController().getScene().sceneChanged.emit(node) return True @staticmethod @@ -174,10 +174,9 @@ class PaintTool(Tool): super().event(event) controller = Application.getInstance().getController() - nodes = Selection.getAllSelectedObjects() - if len(nodes) <= 0: + node = Selection.getSelectedObject(0) + if node is None: return False - node = nodes[0] # Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes if event.type == Event.ToolActivateEvent: diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index 5fb62436c6..cf9d0611f0 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -91,7 +91,7 @@ class PaintView(View): paint_batch = renderer.createRenderBatch(shader=self._paint_shader) renderer.addRenderBatch(paint_batch) - node = Selection.getAllSelectedObjects()[0] + node = Selection.getSelectedObject(0) if node is None: return paint_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData(), normal_transformation=node.getCachedNormalMatrix()) From 40f02dc15fa0cf2a412ca8420c0594c1451b1f01 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Thu, 5 Jun 2025 08:23:22 +0200 Subject: [PATCH 028/159] Defensive coding; deal with degenerate triangles, co-linearity or query pt equal to corner. This shouldn't happen on a well UV-mapped, manifold mesh -- well, unless someone manages to click exactly on one of the triangle corners. Better to get this fixed now then to run into floating point shenanigans later. part of CURA-12543 --- plugins/PaintTool/PaintTool.py | 2 ++ plugins/PaintTool/PaintView.py | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index 06c066e725..afa3b3d09e 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -119,6 +119,8 @@ class PaintTool(Tool): @staticmethod def _get_intersect_ratio_via_pt(a: numpy.ndarray, pt: numpy.ndarray, b: numpy.ndarray, c: numpy.ndarray) -> float: # compute the intersection of (param) A - pt with (param) B - (param) C + if all(a == pt) or all(b == c) or all(a == c) or all(a == b): + return 1.0 # compute unit vectors of directions of lines A and B udir_a = a - pt diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index cf9d0611f0..f72c483340 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -27,9 +27,8 @@ class PaintView(View): self._paint_shader: Optional[ShaderProgram] = None self._paint_texture: Optional[Texture] = None - # FIXME: When the texture UV-unwrapping is done, these two values will need to be set to a proper value (suggest 4096 for both). - self._tex_width = 512 - self._tex_height = 512 + self._tex_width = 2048 + self._tex_height = 2048 self._stroke_undo_stack: List[Tuple[QImage, int, int]] = [] self._stroke_redo_stack: List[Tuple[QImage, int, int]] = [] From 4a87b48084e5f99dfce7be6c86e09c95dc18451c Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Thu, 5 Jun 2025 08:25:20 +0200 Subject: [PATCH 029/159] Stopgap to prevent texture-patch borders from messing up the painting. This code is expandable into the real solution later, see the TODO left in the code by this commit. part of CURA-12543 --- plugins/PaintTool/PaintTool.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index afa3b3d09e..cbb0c97739 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -47,7 +47,8 @@ class PaintTool(Tool): self._ctrl_held: bool = False self._shift_held: bool = False - self._last_text_coords: Optional[Tuple[int, int]] = None + self._last_text_coords: Optional[numpy.ndarray] = None + self._last_face_id: Optional[int] = None def _createBrushPen(self) -> QPen: pen = QPen() @@ -144,10 +145,10 @@ class PaintTool(Tool): def _nodeTransformChanged(self, *args) -> None: self._cache_dirty = True - def _getTexCoordsFromClick(self, node: SceneNode, x: int, y: int) -> Optional[Tuple[float, float]]: + def _getTexCoordsFromClick(self, node: SceneNode, x: int, y: int) -> Tuple[int, Optional[numpy.ndarray]]: face_id = self._selection_pass.getFaceIdAtPosition(x, y) if face_id < 0 or face_id >= node.getMeshData().getFaceCount(): - return None + return face_id, None pt = self._picking_pass.getPickedPosition(x, y).getData() @@ -164,7 +165,7 @@ class PaintTool(Tool): wb /= wt wc /= wt texcoords = wa * ta + wb * tb + wc * tc - return texcoords + return face_id, texcoords def event(self, event: Event) -> bool: """Handle mouse and keyboard events. @@ -219,6 +220,7 @@ class PaintTool(Tool): return False self._mouse_held = False self._last_text_coords = None + self._last_face_id = None return True is_moved = event.type == Event.MouseMoveEvent @@ -265,11 +267,20 @@ class PaintTool(Tool): self._selection_pass.renderFacesMode() - texcoords = self._getTexCoordsFromClick(node, evt.x, evt.y) + face_id, texcoords = self._getTexCoordsFromClick(node, evt.x, evt.y) if texcoords is None: return False if self._last_text_coords is None: self._last_text_coords = texcoords + self._last_face_id = face_id + + if face_id != self._last_face_id: + # TODO: draw two strokes in this case, for the two faces involved + # ... it's worse, for smaller faces we may genuinely require the patch -- and it may even go over _multiple_ patches if the user paints fast enough + # -> for now; make a lookup table for which faces are connected to which, don't split if they are connected, and solve the connection issue(s) later + self._last_text_coords = texcoords + self._last_face_id = face_id + return True w, h = paintview.getUvTexDimensions() sub_image, (start_x, start_y) = self._createStrokeImage( @@ -281,6 +292,7 @@ class PaintTool(Tool): paintview.addStroke(sub_image, start_x, start_y) self._last_text_coords = texcoords + self._last_face_id = face_id Application.getInstance().getController().getScene().sceneChanged.emit(node) return True From 44042eef6aa28e9108f2948f8a5a0b28adbb5808 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Thu, 5 Jun 2025 12:45:34 +0200 Subject: [PATCH 030/159] Start to set up UV-unwrapping. Needs the new libreary set up for that. part of CURA-12528 --- plugins/3MFReader/ThreeMFReader.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index 45ab2e7d2f..98cae593e1 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -141,6 +141,9 @@ class ThreeMFReader(MeshReader): # The filename is used to give the user the option to reload the file if it is changed on disk # It is only set for the root node of the 3mf file mesh_builder.setFileName(file_name) + + mesh_builder.unwrapNewUvs() + mesh_data = mesh_builder.build() if len(mesh_data.getVertices()): From b358b93b690bd2106c9c68f50da5f3c73b11fafe Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 10 Jun 2025 08:30:43 +0200 Subject: [PATCH 031/159] Use proper English word for "plane" CURA-12544 The word in French to describe a geometric flat surface if "plan", which is a valid word in English but it doesn't have the same meaning, this got me confused. So replacing "plan" by "plane" because we are actually dealing with a geometrical "plane" (although it doesn't fly). --- plugins/SimulationView/layers3d_shadow.shader | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/SimulationView/layers3d_shadow.shader b/plugins/SimulationView/layers3d_shadow.shader index 47637cfb20..0cf3e4f75a 100644 --- a/plugins/SimulationView/layers3d_shadow.shader +++ b/plugins/SimulationView/layers3d_shadow.shader @@ -103,8 +103,8 @@ geometry41core = vec4 g_vertex_offset_vert; vec3 g_vertex_normal_horz_head; vec4 g_vertex_offset_horz_head; - vec3 g_axial_plan_vector; - vec3 g_radial_plan_vector; + vec3 g_axial_plane_vector; + vec3 g_radial_plane_vector; float size_x; float size_y; @@ -143,25 +143,25 @@ geometry41core = if (g_vertex_delta.y == 0.0) { - // vector is in the horizontal plan, radial vector is a simple rotation around Y axis - g_radial_plan_vector = vec3(g_vertex_delta.z, 0.0, -g_vertex_delta.x); + // vector is in the horizontal plane, radial vector is a simple rotation around Y axis + g_radial_plane_vector = vec3(g_vertex_delta.z, 0.0, -g_vertex_delta.x); } else if(g_vertex_delta.x == 0.0 && g_vertex_delta.z == 0.0) { // delta vector is purely vertical, display the line rotated vertically so that it is visible in front and side views - g_radial_plan_vector = vec3(1.0, 0.0, -1.0); + g_radial_plane_vector = vec3(1.0, 0.0, -1.0); } else { // delta vector is completely 3D - g_axial_plan_vector = vec3(g_vertex_delta.x, 0.0, g_vertex_delta.z); // Vector projected in the horizontal plan - g_radial_plan_vector = cross(g_vertex_delta, g_axial_plan_vector); // Radial vector in the horizontal plan, pointing right. + g_axial_plane_vector = vec3(g_vertex_delta.x, 0.0, g_vertex_delta.z); // Vector projected in the horizontal plane + g_radial_plane_vector = cross(g_vertex_delta, g_axial_plane_vector); // Radial vector in the horizontal plane, pointing right. } g_vertex_normal_horz_head = normalize(g_vertex_delta); //Lengthwise normal vector g_vertex_offset_horz_head = vec4(g_vertex_normal_horz_head * size_x, 0.0); //Lengthwise offset vector - g_vertex_normal_horz = normalize(g_radial_plan_vector); //Normal vector pointing right. + g_vertex_normal_horz = normalize(g_radial_plane_vector); //Normal vector pointing right. g_vertex_offset_horz = vec4(g_vertex_normal_horz * size_x, 0.0); //Offset vector pointing right. g_vertex_normal_vert = vec3(0.0, 1.0, 0.0); //Upwards normal vector. From f84923185ded2dde8f614a7ab6f0e50d6290b343 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 10 Jun 2025 08:30:43 +0200 Subject: [PATCH 032/159] Use proper English word for "plane" CURA-12544 The word in French to describe a geometric flat surface if "plan", which is a valid word in English but it doesn't have the same meaning, this got me confused. So replacing "plan" by "plane" because we are actually dealing with a geometrical "plane" (although it doesn't fly). --- plugins/SimulationView/layers3d_shadow.shader | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/SimulationView/layers3d_shadow.shader b/plugins/SimulationView/layers3d_shadow.shader index 47637cfb20..0cf3e4f75a 100644 --- a/plugins/SimulationView/layers3d_shadow.shader +++ b/plugins/SimulationView/layers3d_shadow.shader @@ -103,8 +103,8 @@ geometry41core = vec4 g_vertex_offset_vert; vec3 g_vertex_normal_horz_head; vec4 g_vertex_offset_horz_head; - vec3 g_axial_plan_vector; - vec3 g_radial_plan_vector; + vec3 g_axial_plane_vector; + vec3 g_radial_plane_vector; float size_x; float size_y; @@ -143,25 +143,25 @@ geometry41core = if (g_vertex_delta.y == 0.0) { - // vector is in the horizontal plan, radial vector is a simple rotation around Y axis - g_radial_plan_vector = vec3(g_vertex_delta.z, 0.0, -g_vertex_delta.x); + // vector is in the horizontal plane, radial vector is a simple rotation around Y axis + g_radial_plane_vector = vec3(g_vertex_delta.z, 0.0, -g_vertex_delta.x); } else if(g_vertex_delta.x == 0.0 && g_vertex_delta.z == 0.0) { // delta vector is purely vertical, display the line rotated vertically so that it is visible in front and side views - g_radial_plan_vector = vec3(1.0, 0.0, -1.0); + g_radial_plane_vector = vec3(1.0, 0.0, -1.0); } else { // delta vector is completely 3D - g_axial_plan_vector = vec3(g_vertex_delta.x, 0.0, g_vertex_delta.z); // Vector projected in the horizontal plan - g_radial_plan_vector = cross(g_vertex_delta, g_axial_plan_vector); // Radial vector in the horizontal plan, pointing right. + g_axial_plane_vector = vec3(g_vertex_delta.x, 0.0, g_vertex_delta.z); // Vector projected in the horizontal plane + g_radial_plane_vector = cross(g_vertex_delta, g_axial_plane_vector); // Radial vector in the horizontal plane, pointing right. } g_vertex_normal_horz_head = normalize(g_vertex_delta); //Lengthwise normal vector g_vertex_offset_horz_head = vec4(g_vertex_normal_horz_head * size_x, 0.0); //Lengthwise offset vector - g_vertex_normal_horz = normalize(g_radial_plan_vector); //Normal vector pointing right. + g_vertex_normal_horz = normalize(g_radial_plane_vector); //Normal vector pointing right. g_vertex_offset_horz = vec4(g_vertex_normal_horz * size_x, 0.0); //Offset vector pointing right. g_vertex_normal_vert = vec3(0.0, 1.0, 0.0); //Upwards normal vector. From 65b0e4f080d0dceb37aeddc8ca9f4d8cc52e6883 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 11 Jun 2025 10:51:47 +0200 Subject: [PATCH 033/159] Add specific message when sending a print to an inactive printer CURA-12557 --- .../src/Cloud/CloudApiClient.py | 22 +++++++++++++++++-- .../src/Cloud/CloudOutputDevice.py | 16 +++++++++----- .../PrintJobUploadPrinterInactiveMessage.py | 20 +++++++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadPrinterInactiveMessage.py diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py index 3c8e53b2e9..0831ceebd3 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py @@ -163,7 +163,7 @@ class CloudApiClient: scope=self._scope, data=b"", callback=self._parseCallback(on_finished, CloudPrintResponse), - error_callback=on_error, + error_callback=self._parseError(on_error), timeout=self.DEFAULT_REQUEST_TIMEOUT) def doPrintJobAction(self, cluster_id: str, cluster_job_id: str, action: str, @@ -256,7 +256,6 @@ class CloudApiClient: """Creates a callback function so that it includes the parsing of the response into the correct model. The callback is added to the 'finished' signal of the reply. - :param reply: The reply that should be listened to. :param on_finished: The callback in case the response is successful. Depending on the endpoint it will be either a list or a single item. :param model: The type of the model to convert the response to. @@ -281,6 +280,25 @@ class CloudApiClient: self._anti_gc_callbacks.append(parse) return parse + def _parseError(self, + on_error: Callable[[CloudError, "QNetworkReply.NetworkError", int], None]) -> Callable[[QNetworkReply, "QNetworkReply.NetworkError"], None]: + + """Creates a callback function so that it includes the parsing of an explicit error response into the correct model. + + :param on_error: The callback in case the response gives an explicit error + """ + + def parse(reply: QNetworkReply, error: "QNetworkReply.NetworkError") -> None: + + self._anti_gc_callbacks.remove(parse) + + http_code, response = self._parseReply(reply) + result = CloudError(**response["errors"][0]) + on_error(result, error, http_code) + + self._anti_gc_callbacks.append(parse) + return parse + @classmethod def getMachineIDMap(cls) -> Dict[str, str]: if cls._machine_id_to_name is None: diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index 090355a3c0..df486822b7 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -27,9 +27,11 @@ from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOut from ..Messages.PrintJobUploadBlockedMessage import PrintJobUploadBlockedMessage from ..Messages.PrintJobUploadErrorMessage import PrintJobUploadErrorMessage from ..Messages.PrintJobUploadQueueFullMessage import PrintJobUploadQueueFullMessage +from ..Messages.PrintJobUploadPrinterInactiveMessage import PrintJobUploadPrinterInactiveMessage from ..Messages.PrintJobUploadSuccessMessage import PrintJobUploadSuccessMessage from ..Models.Http.CloudClusterResponse import CloudClusterResponse from ..Models.Http.CloudClusterStatus import CloudClusterStatus +from ..Models.Http.CloudError import CloudError from ..Models.Http.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest from ..Models.Http.CloudPrintResponse import CloudPrintResponse from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse @@ -291,19 +293,21 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self.writeFinished.emit() - def _onPrintUploadSpecificError(self, reply: "QNetworkReply", _: "QNetworkReply.NetworkError"): + def _onPrintUploadSpecificError(self, error: CloudError, _: "QNetworkReply.NetworkError", http_error: int): """ Displays a message when an error occurs specific to uploading print job (i.e. queue is full). """ - error_code = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) - if error_code == 409: - PrintJobUploadQueueFullMessage().show() + if http_error == 409: + if error.code == "printerInactive": + PrintJobUploadPrinterInactiveMessage().show() + else: + PrintJobUploadQueueFullMessage().show() else: PrintJobUploadErrorMessage(I18N_CATALOG.i18nc("@error:send", "Unknown error code when uploading print job: {0}", - error_code)).show() + http_error)).show() - Logger.log("w", "Upload of print job failed specifically with error code {}".format(error_code)) + Logger.log("w", "Upload of print job failed specifically with error code {}".format(http_error)) self._progress.hide() self._pre_upload_print_job = None diff --git a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadPrinterInactiveMessage.py b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadPrinterInactiveMessage.py new file mode 100644 index 0000000000..324259eea4 --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadPrinterInactiveMessage.py @@ -0,0 +1,20 @@ +# Copyright (c) 2020 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from UM import i18nCatalog +from UM.Message import Message + + +I18N_CATALOG = i18nCatalog("cura") + + +class PrintJobUploadPrinterInactiveMessage(Message): + """Message shown when uploading a print job to a cluster and the printer is inactive.""" + + def __init__(self) -> None: + super().__init__( + text = I18N_CATALOG.i18nc("@info:status", "The printer is inactive and cannot accept a new print job."), + title = I18N_CATALOG.i18nc("@info:title", "Printer inactive"), + lifetime = 10, + message_type=Message.MessageType.ERROR + ) From 2e9999ed2dd36d0e1e2f5da474e18ef1f5018a77 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 11 Jun 2025 13:51:45 +0200 Subject: [PATCH 034/159] Display the printer activation status CURA-12557 --- cura/Settings/MachineManager.py | 14 +++++++ .../src/Cloud/CloudOutputDevice.py | 13 ++++++ .../src/Models/Http/CloudClusterResponse.py | 6 ++- .../src/Models/Http/CloudClusterStatus.py | 2 + .../qml/PrinterSelector/MachineSelector.qml | 40 ++++++++++++++++--- resources/themes/cura-light/theme.json | 1 + 6 files changed, 69 insertions(+), 7 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 986608cd49..1bdb32f4ac 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -183,10 +183,16 @@ class MachineManager(QObject): self.setActiveMachine(active_machine_id) def _onOutputDevicesChanged(self) -> None: + for printer_output_device in self._printer_output_devices: + if hasattr(printer_output_device, "cloudActiveChanged"): + printer_output_device.cloudActiveChanged.disconnect(self.printerConnectedStatusChanged) + self._printer_output_devices = [] for printer_output_device in self._application.getOutputDeviceManager().getOutputDevices(): if isinstance(printer_output_device, PrinterOutputDevice): self._printer_output_devices.append(printer_output_device) + if hasattr(printer_output_device, "cloudActiveChanged"): + printer_output_device.cloudActiveChanged.connect(self.printerConnectedStatusChanged) self.outputDevicesChanged.emit() @@ -569,6 +575,14 @@ class MachineManager(QObject): def activeMachineIsUsingCloudConnection(self) -> bool: return self.activeMachineHasCloudConnection and not self.activeMachineHasNetworkConnection + @pyqtProperty(bool, notify = printerConnectedStatusChanged) + def activeMachineIsCloudActive(self) -> bool: + if not self._printer_output_devices: + return True + + first_printer = self._printer_output_devices[0] + return True if not hasattr(first_printer, 'cloudActive') else first_printer.cloudActive + def activeMachineNetworkKey(self) -> str: if self._global_container_stack: return self._global_container_stack.getMetaDataEntry("um_network_key", "") diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index df486822b7..020cafacd8 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -65,6 +65,8 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): # Therefore, we create a private signal used to trigger the printersChanged signal. _cloudClusterPrintersChanged = pyqtSignal() + cloudActiveChanged = pyqtSignal() + def __init__(self, api_client: CloudApiClient, cluster: CloudClusterResponse, parent: QObject = None) -> None: """Creates a new cloud output device @@ -113,6 +115,9 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._pre_upload_print_job = None # type: Optional[CloudPrintJobResponse] self._uploaded_print_job = None # type: Optional[CloudPrintJobResponse] + # Whether the printer is active, i.e. authorized for use i.r.t to workspace limitations + self._active = cluster.display_status != "inactive" + CuraApplication.getInstance().getBackend().backendDone.connect(self._resetPrintJob) CuraApplication.getInstance().getController().getScene().sceneChanged.connect(self._onSceneChanged) @@ -192,6 +197,10 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._received_print_jobs = status.print_jobs self._updatePrintJobs(status.print_jobs) + if status.active != self._active: + self._active = status.active + self.cloudActiveChanged.emit() + def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, filter_by_machine: bool = False, **kwargs) -> None: @@ -436,6 +445,10 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): root_url_prefix = "-staging" if self._account.is_staging else "" return f"https://digitalfactory{root_url_prefix}.ultimaker.com/app/jobs/{self.clusterData.cluster_id}" + @pyqtProperty(bool, notify = cloudActiveChanged) + def cloudActive(self) -> bool: + return self._active + def __del__(self): CuraApplication.getInstance().getBackend().backendDone.disconnect(self._resetPrintJob) CuraApplication.getInstance().getController().getScene().sceneChanged.disconnect(self._onSceneChanged) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py index 713582b8ad..a1f22f7b36 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py @@ -10,7 +10,7 @@ class CloudClusterResponse(BaseModel): """Class representing a cloud connected cluster.""" def __init__(self, cluster_id: str, host_guid: str, host_name: str, is_online: bool, status: str, - host_internal_ip: Optional[str] = None, host_version: Optional[str] = None, + display_status: str, host_internal_ip: Optional[str] = None, host_version: Optional[str] = None, friendly_name: Optional[str] = None, printer_type: str = "ultimaker3", printer_count: int = 1, capabilities: Optional[List[str]] = None, **kwargs) -> None: """Creates a new cluster response object. @@ -20,6 +20,7 @@ class CloudClusterResponse(BaseModel): :param host_name: The name of the printer as configured during the Wi-Fi setup. Used as identifier for end users. :param is_online: Whether this cluster is currently connected to the cloud. :param status: The status of the cluster authentication (active or inactive). + :param display_status: The display status of the cluster. :param host_version: The firmware version of the cluster host. This is where the Stardust client is running on. :param host_internal_ip: The internal IP address of the host printer. :param friendly_name: The human readable name of the host printer. @@ -31,6 +32,7 @@ class CloudClusterResponse(BaseModel): self.host_guid = host_guid self.host_name = host_name self.status = status + self.display_status = display_status self.is_online = is_online self.host_version = host_version self.host_internal_ip = host_internal_ip @@ -51,5 +53,5 @@ class CloudClusterResponse(BaseModel): Convenience function for printing when debugging. :return: A human-readable representation of the data in this object. """ - return str({k: v for k, v in self.__dict__.items() if k in {"cluster_id", "host_guid", "host_name", "status", "is_online", "host_version", "host_internal_ip", "friendly_name", "printer_type", "printer_count", "capabilities"}}) + return str({k: v for k, v in self.__dict__.items() if k in {"cluster_id", "host_guid", "host_name", "status", "display_status", "is_online", "host_version", "host_internal_ip", "friendly_name", "printer_type", "printer_count", "capabilities"}}) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py index 5cd151d8ef..34249dc67a 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py @@ -14,6 +14,7 @@ class CloudClusterStatus(BaseModel): def __init__(self, printers: List[Union[ClusterPrinterStatus, Dict[str, Any]]], print_jobs: List[Union[ClusterPrintJobStatus, Dict[str, Any]]], generated_time: Union[str, datetime], + unavailable: bool = False, **kwargs) -> None: """Creates a new cluster status model object. @@ -23,6 +24,7 @@ class CloudClusterStatus(BaseModel): """ self.generated_time = self.parseDate(generated_time) + self.active = not unavailable self.printers = self.parseModels(ClusterPrinterStatus, printers) self.print_jobs = self.parseModels(ClusterPrintJobStatus, print_jobs) super().__init__(**kwargs) diff --git a/resources/qml/PrinterSelector/MachineSelector.qml b/resources/qml/PrinterSelector/MachineSelector.qml index 1bad1d70bc..7acdd9573b 100644 --- a/resources/qml/PrinterSelector/MachineSelector.qml +++ b/resources/qml/PrinterSelector/MachineSelector.qml @@ -16,6 +16,7 @@ Cura.ExpandablePopup property bool isConnectedCloudPrinter: machineManager.activeMachineHasCloudConnection property bool isCloudRegistered: machineManager.activeMachineHasCloudRegistration property bool isGroup: machineManager.activeMachineIsGroup + property bool isCloudActive: machineManager.activeMachineIsCloudActive property string machineName: { if (isNetworkPrinter && machineManager.activeMachineNetworkGroupName != "") { @@ -40,7 +41,14 @@ Cura.ExpandablePopup } else if (isConnectedCloudPrinter && Cura.API.connectionStatus.isInternetReachable) { - return "printer_cloud_connected" + if (isCloudActive) + { + return "printer_cloud_connected" + } + else + { + return "printer_cloud_inactive" + } } else if (isCloudRegistered) { @@ -53,7 +61,7 @@ Cura.ExpandablePopup } function getConnectionStatusMessage() { - if (connectionStatus == "printer_cloud_not_available") + if (connectionStatus === "printer_cloud_not_available") { if(Cura.API.connectionStatus.isInternetReachable) { @@ -78,6 +86,10 @@ Cura.ExpandablePopup return catalog.i18nc("@status", "The cloud connection is currently unavailable. Please check your internet connection.") } } + else if(connectionStatus === "printer_cloud_inactive") + { + return catalog.i18nc("@status", "This printer is deactivated and can not accept commands or jobs.") + } else { return "" @@ -130,14 +142,18 @@ Cura.ExpandablePopup source: { - if (connectionStatus == "printer_connected") + if (connectionStatus === "printer_connected") { return UM.Theme.getIcon("CheckBlueBG", "low") } - else if (connectionStatus == "printer_cloud_connected" || connectionStatus == "printer_cloud_not_available") + else if (connectionStatus === "printer_cloud_connected" || connectionStatus === "printer_cloud_not_available") { return UM.Theme.getIcon("CloudBadge", "low") } + else if (connectionStatus === "printer_cloud_inactive") + { + return UM.Theme.getIcon("WarningBadge", "low") + } else { return "" @@ -147,7 +163,21 @@ Cura.ExpandablePopup width: UM.Theme.getSize("printer_status_icon").width height: UM.Theme.getSize("printer_status_icon").height - color: connectionStatus == "printer_cloud_not_available" ? UM.Theme.getColor("cloud_unavailable") : UM.Theme.getColor("primary") + color: + { + if (connectionStatus === "printer_cloud_not_available") + { + return UM.Theme.getColor("cloud_unavailable") + } + else if(connectionStatus === "printer_cloud_inactive") + { + return UM.Theme.getColor("cloud_inactive") + } + else + { + return UM.Theme.getColor("primary") + } + } visible: (isNetworkPrinter || isCloudRegistered) && source != "" diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 1ae316f96c..8d09d7837d 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -496,6 +496,7 @@ "monitor_carousel_dot_current": [119, 119, 119, 255], "cloud_unavailable": [153, 153, 153, 255], + "cloud_inactive": "warning", "connection_badge_background": [255, 255, 255, 255], "warning_badge_background": [0, 0, 0, 255], "error_badge_background": [255, 255, 255, 255], From a739fd21f5d705ec639a538a9c1891e03658087a Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 11 Jun 2025 15:48:09 +0200 Subject: [PATCH 035/159] Display inactive DL projects as disabled CURA-12557 --- .../resources/qml/ProjectSummaryCard.qml | 36 ++++++++++++++----- .../resources/qml/SelectProjectPage.qml | 29 ++++++++++----- .../src/DigitalFactoryProjectModel.py | 3 ++ .../src/DigitalFactoryProjectResponse.py | 2 ++ resources/themes/cura-light/theme.json | 2 +- 5 files changed, 55 insertions(+), 17 deletions(-) diff --git a/plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml b/plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml index ca836ee21d..133cc0edde 100644 --- a/plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml +++ b/plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml @@ -11,10 +11,10 @@ Cura.RoundedRectangle width: parent.width height: projectImage.height + 2 * UM.Theme.getSize("default_margin").width cornerSide: Cura.RoundedRectangle.Direction.All - border.color: UM.Theme.getColor("lining") + border.color: enabled ? UM.Theme.getColor("lining") : UM.Theme.getColor("action_button_disabled_border") border.width: UM.Theme.getSize("default_lining").width radius: UM.Theme.getSize("default_radius").width - color: UM.Theme.getColor("main_background") + color: getBackgoundColor() signal clicked() property alias imageSource: projectImage.source property alias projectNameText: displayNameLabel.text @@ -22,17 +22,18 @@ Cura.RoundedRectangle property alias projectLastUpdatedText: lastUpdatedLabel.text property alias cardMouseAreaEnabled: cardMouseArea.enabled - onVisibleChanged: color = UM.Theme.getColor("main_background") + onVisibleChanged: color = getBackgroundColor() MouseArea { id: cardMouseArea anchors.fill: parent - hoverEnabled: true - onEntered: base.color = UM.Theme.getColor("action_button_hovered") - onExited: base.color = UM.Theme.getColor("main_background") + hoverEnabled: base.enabled + onEntered: color = getBackgroundColor() + onExited: color = getBackgroundColor() onClicked: base.clicked() } + Row { id: projectInformationRow @@ -73,7 +74,7 @@ Cura.RoundedRectangle width: parent.width height: Math.round(parent.height / 3) elide: Text.ElideRight - color: UM.Theme.getColor("small_button_text") + color: base.enabled ? UM.Theme.getColor("small_button_text") : UM.Theme.getColor("text_disabled") } UM.Label @@ -82,8 +83,27 @@ Cura.RoundedRectangle width: parent.width height: Math.round(parent.height / 3) elide: Text.ElideRight - color: UM.Theme.getColor("small_button_text") + color: base.enabled ? UM.Theme.getColor("small_button_text") : UM.Theme.getColor("text_disabled") } } } + + function getBackgroundColor() + { + if(enabled) + { + if(cardMouseArea.containsMouse) + { + return UM.Theme.getColor("action_button_hovered") + } + else + { + return UM.Theme.getColor("main_background") + } + } + else + { + return UM.Theme.getColor("action_button_disabled") + } + } } \ No newline at end of file diff --git a/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml b/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml index faceb4df23..2d0bd30f2b 100644 --- a/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml +++ b/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml @@ -159,17 +159,30 @@ Item Repeater { model: manager.digitalFactoryProjectModel - delegate: ProjectSummaryCard + delegate: Item { - id: projectSummaryCard - imageSource: model.thumbnailUrl || "../images/placeholder.svg" - projectNameText: model.displayName - projectUsernameText: model.username - projectLastUpdatedText: "Last updated: " + model.lastUpdated + width: parent.width + height: projectSummaryCard.height - onClicked: + UM.TooltipArea { - manager.selectedProjectIndex = index + anchors.fill: parent + text: "This project is inactive and cannot be used." + enabled: !model.active + } + + ProjectSummaryCard + { + id: projectSummaryCard + imageSource: model.thumbnailUrl || "../images/placeholder.svg" + projectNameText: model.displayName + projectUsernameText: model.username + projectLastUpdatedText: "Last updated: " + model.lastUpdated + enabled: model.active + + onClicked: { + manager.selectedProjectIndex = index + } } } } diff --git a/plugins/DigitalLibrary/src/DigitalFactoryProjectModel.py b/plugins/DigitalLibrary/src/DigitalFactoryProjectModel.py index bd12a4ca12..7140657508 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryProjectModel.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryProjectModel.py @@ -17,6 +17,7 @@ class DigitalFactoryProjectModel(ListModel): ThumbnailUrlRole = Qt.ItemDataRole.UserRole + 5 UsernameRole = Qt.ItemDataRole.UserRole + 6 LastUpdatedRole = Qt.ItemDataRole.UserRole + 7 + ActiveRole = Qt.ItemDataRole.UserRole + 8 dfProjectModelChanged = pyqtSignal() @@ -28,6 +29,7 @@ class DigitalFactoryProjectModel(ListModel): self.addRoleName(self.ThumbnailUrlRole, "thumbnailUrl") self.addRoleName(self.UsernameRole, "username") self.addRoleName(self.LastUpdatedRole, "lastUpdated") + self.addRoleName(self.ActiveRole, "active") self._projects = [] # type: List[DigitalFactoryProjectResponse] def setProjects(self, df_projects: List[DigitalFactoryProjectResponse]) -> None: @@ -59,5 +61,6 @@ class DigitalFactoryProjectModel(ListModel): "thumbnailUrl": project.thumbnail_url, "username": project.username, "lastUpdated": project.last_updated.strftime(PROJECT_UPDATED_AT_DATETIME_FORMAT) if project.last_updated else "", + "active": project.active, }) self.dfProjectModelChanged.emit() diff --git a/plugins/DigitalLibrary/src/DigitalFactoryProjectResponse.py b/plugins/DigitalLibrary/src/DigitalFactoryProjectResponse.py index bef90e5125..303271f211 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryProjectResponse.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryProjectResponse.py @@ -28,6 +28,7 @@ class DigitalFactoryProjectResponse(BaseModel): team_ids: Optional[List[str]] = None, status: Optional[str] = None, technical_requirements: Optional[Dict[str, Any]] = None, + is_inactive: bool = False, **kwargs) -> None: """ Creates a new digital factory project response object @@ -56,6 +57,7 @@ class DigitalFactoryProjectResponse(BaseModel): self.last_updated = datetime.strptime(last_updated, DIGITAL_FACTORY_RESPONSE_DATETIME_FORMAT) if last_updated else None self.status = status self.technical_requirements = technical_requirements + self.active = not is_inactive super().__init__(**kwargs) def __str__(self) -> str: diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 8d09d7837d..f8686aafcf 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -496,7 +496,7 @@ "monitor_carousel_dot_current": [119, 119, 119, 255], "cloud_unavailable": [153, 153, 153, 255], - "cloud_inactive": "warning", + "cloud_inactive": [253, 209, 58, 255], "connection_badge_background": [255, 255, 255, 255], "warning_badge_background": [0, 0, 0, 255], "error_badge_background": [255, 255, 255, 255], From b6b2da0c148af19614cf90537dd3599263fbc316 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 12 Jun 2025 08:39:59 +0200 Subject: [PATCH 036/159] Change wording as suggested CURA-11978 "Unretraction" is a barbaric word, better use "Priming" instead --- plugins/SimulationView/SimulationViewMenuComponent.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/SimulationView/SimulationViewMenuComponent.qml b/plugins/SimulationView/SimulationViewMenuComponent.qml index 0e254a005b..78b0b2b74f 100644 --- a/plugins/SimulationView/SimulationViewMenuComponent.qml +++ b/plugins/SimulationView/SimulationViewMenuComponent.qml @@ -229,7 +229,7 @@ Cura.ExpandableComponent { const travelsTypesModel = [ { - label: catalog.i18nc("@label", "Unretracted"), + label: catalog.i18nc("@label", "Not retracted"), colorId: "layerview_move_combing" }, { @@ -241,7 +241,7 @@ Cura.ExpandableComponent colorId: "layerview_move_while_retracting" }, { - label: catalog.i18nc("@label", "Unretracting"), + label: catalog.i18nc("@label", "Priming"), colorId: "layerview_move_while_unretracting" } ]; From 53b91a7b4841456f37021ff80c766699206ac234 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 12 Jun 2025 16:40:28 +0200 Subject: [PATCH 037/159] Add setting to keep retracting during travel CURA-11978 --- resources/definitions/fdmprinter.def.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 3ad11933e6..6747bf9ceb 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -9324,6 +9324,16 @@ "settable_per_mesh": false, "settable_per_extruder": true }, + "keep_retracting_during_travel": + { + "label": "Keep Retracting During Travel", + "description": "When retraction during travel is enabled, and there is more than enough time to perform a full retract during a travel move, spread the retraction over the whole travel move with a lower retraction speed, so that we do not travel with a non-retracting nozzle. This can help reducing oozing.", + "type": "bool", + "default_value": false, + "enabled": "retraction_enable and not machine_firmware_retract and machine_gcode_flavor != \"UltiGCode\" and machine_gcode_flavor != \"BFB\" and retraction_during_travel_ratio > 0", + "settable_per_mesh": false, + "settable_per_extruder": true + }, "prime_during_travel_ratio": { "label": "Prime During Travel Move", From d0947c5fb2634e9b7a046dfd18050bbaadfdacff Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 13 Jun 2025 16:17:58 +0200 Subject: [PATCH 038/159] Include new print feature type CURA-11978 --- cura/LayerPolygon.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cura/LayerPolygon.py b/cura/LayerPolygon.py index c4d57c07a0..cd4642d719 100644 --- a/cura/LayerPolygon.py +++ b/cura/LayerPolygon.py @@ -25,7 +25,8 @@ class LayerPolygon: PrimeTowerType = 11 MoveWhileRetractingType = 12 MoveWhileUnretractingType = 13 - __number_of_types = 14 + StationaryRetractUnretract = 14 + __number_of_types = 15 __jump_map = numpy.logical_or(numpy.logical_or(numpy.logical_or( numpy.arange(__number_of_types) == NoneType, @@ -281,6 +282,7 @@ class LayerPolygon: theme.getColor("layerview_prime_tower").getRgbF(), # PrimeTowerType theme.getColor("layerview_move_while_retracting").getRgbF(), # MoveWhileRetracting theme.getColor("layerview_move_while_unretracting").getRgbF(), # MoveWhileUnretracting + theme.getColor("layerview_move_retraction").getRgbF(), # StationaryRetractUnretract ]) return cls.__color_map From 2390067d694b425eb87a3830ce3647258f07fc1c Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 17 Jun 2025 09:20:59 +0200 Subject: [PATCH 039/159] Restore basic themes CURA-12544 Mistakenly commited after applying the daily rotated colors --- .../themes/cura-dark-colorblind/theme.json | 27 +- resources/themes/cura-dark/theme.json | 213 +++++- .../themes/cura-light-colorblind/theme.json | 30 +- resources/themes/cura-light/theme.json | 681 +++++++++++++++++- resources/themes/daily_test_colors.json | 16 + 5 files changed, 963 insertions(+), 4 deletions(-) create mode 100644 resources/themes/daily_test_colors.json diff --git a/resources/themes/cura-dark-colorblind/theme.json b/resources/themes/cura-dark-colorblind/theme.json index 4443111b60..4a006ee836 100644 --- a/resources/themes/cura-dark-colorblind/theme.json +++ b/resources/themes/cura-dark-colorblind/theme.json @@ -1 +1,26 @@ -{"metadata": {"name": "Colorblind Assist Dark", "inherits": "cura-dark"}, "colors": {"x_axis": [212, 0, 0, 255], "y_axis": [64, 64, 255, 255], "model_overhang": [200, 0, 255, 255], "xray": [26, 26, 62, 255], "xray_error": [255, 0, 0, 255], "layerview_inset_0": [255, 64, 0, 255], "layerview_inset_x": [0, 156, 128, 255], "layerview_skin": [255, 255, 86, 255], "layerview_support": [255, 255, 0, 255], "layerview_infill": [0, 255, 255, 255], "layerview_support_infill": [0, 200, 200, 255], "layerview_move_retraction": [0, 100, 255, 255], "main_window_header_background": [192, 199, 65, 255]}} \ No newline at end of file +{ + "metadata": { + "name": "Colorblind Assist Dark", + "inherits": "cura-dark" + }, + + "colors": { + "x_axis": [212, 0, 0, 255], + "y_axis": [64, 64, 255, 255], + + "model_overhang": [200, 0, 255, 255], + + "xray": [26, 26, 62, 255], + "xray_error": [255, 0, 0, 255], + + "layerview_inset_0": [255, 64, 0, 255], + "layerview_inset_x": [0, 156, 128, 255], + "layerview_skin": [255, 255, 86, 255], + "layerview_support": [255, 255, 0, 255], + + "layerview_infill": [0, 255, 255, 255], + "layerview_support_infill": [0, 200, 200, 255], + + "layerview_move_retraction": [0, 100, 255, 255] + } +} diff --git a/resources/themes/cura-dark/theme.json b/resources/themes/cura-dark/theme.json index 29be47e697..64c3e002a9 100644 --- a/resources/themes/cura-dark/theme.json +++ b/resources/themes/cura-dark/theme.json @@ -1 +1,212 @@ -{"metadata": {"name": "UltiMaker Dark", "inherits": "cura-light"}, "base_colors": {"background_1": [31, 31, 32, 255], "background_2": [57, 57, 58, 255], "background_3": [85, 85, 87, 255], "background_4": [23, 23, 23, 255], "accent_1": [25, 110, 240, 255], "accent_2": [16, 70, 156, 255], "border_main": [118, 118, 119, 255], "border_accent_1": [255, 255, 255, 255], "border_accent_2": [243, 243, 243, 255], "border_field": [57, 57, 58, 255], "text_default": [255, 255, 255, 255], "text_disabled": [118, 118, 118, 255], "text_primary_button": [255, 255, 255, 255], "text_secondary_button": [255, 255, 255, 255], "text_link_hover": [156, 195, 255, 255], "text_lighter": [243, 243, 243, 255], "um_green_1": [233, 245, 237, 255], "um_green_5": [36, 162, 73, 255], "um_green_9": [31, 44, 36, 255], "um_red_1": [251, 232, 233, 255], "um_red_5": [218, 30, 40, 255], "um_red_9": [59, 31, 33, 255], "um_orange_1": [255, 235, 221, 255], "um_orange_5": [252, 123, 30, 255], "um_orange_9": [64, 45, 32, 255], "um_yellow_1": [255, 248, 225, 255], "um_yellow_5": [253, 209, 58, 255], "um_yellow_9": [64, 58, 36, 255]}, "colors": {"main_background": "background_1", "detail_background": "background_2", "message_background": "background_1", "wide_lining": [31, 36, 39, 255], "thick_lining": [255, 255, 255, 60], "lining": "border_main", "viewport_overlay": "background_1", "primary_text": "text_default", "secondary": [95, 95, 95, 255], "expandable_active": "background_2", "expandable_hover": "background_2", "secondary_button": "background_1", "secondary_button_hover": "background_3", "secondary_button_text": "text_secondary_button", "icon": "text_default", "toolbar_background": "background_1", "toolbar_button_active": "background_3", "toolbar_button_hover": "background_3", "toolbar_button_active_hover": "background_3", "main_window_header_button_background_inactive": "background_4", "main_window_header_button_text_inactive": "text_primary_button", "main_window_header_button_text_active": "background_4", "main_window_header_background": [192, 199, 65, 255], "main_window_header_background_gradient": "background_4", "main_window_header_button_background_hovered": [46, 46, 46, 255], "account_sync_state_icon": [255, 255, 255, 204], "machine_selector_printer_icon": [204, 204, 204, 255], "text": "text_default", "text_detail": [255, 255, 255, 172], "text_link": "accent_1", "text_inactive": [118, 118, 118, 255], "text_hover": [255, 255, 255, 255], "text_scene": [250, 250, 250, 255], "text_scene_hover": [255, 255, 255, 255], "printer_type_label_background": [95, 95, 95, 255], "error": [212, 31, 53, 255], "disabled": [32, 32, 32, 255], "button": [39, 44, 48, 255], "button_hover": [39, 44, 48, 255], "button_text": "text_default", "button_disabled": [39, 44, 48, 255], "button_disabled_text": [255, 255, 255, 101], "small_button_text": [255, 255, 255, 197], "small_button_text_hover": [255, 255, 255, 255], "button_tooltip": [39, 44, 48, 255], "tab_checked": [39, 44, 48, 255], "tab_checked_border": [255, 255, 255, 30], "tab_checked_text": [255, 255, 255, 255], "tab_unchecked": [39, 44, 48, 255], "tab_unchecked_border": [255, 255, 255, 30], "tab_unchecked_text": [255, 255, 255, 101], "tab_hovered": [39, 44, 48, 255], "tab_hovered_border": [255, 255, 255, 30], "tab_hovered_text": [255, 255, 255, 255], "tab_active": [39, 44, 48, 255], "tab_active_border": [255, 255, 255, 30], "tab_active_text": [255, 255, 255, 255], "tab_background": [39, 44, 48, 255], "action_button": "background_1", "action_button_text": [255, 255, 255, 200], "action_button_border": "border_main", "action_button_hovered": [79, 85, 89, 255], "action_button_hovered_text": "text_default", "action_button_hovered_border": "border_main", "action_button_active": [39, 44, 48, 30], "action_button_active_text": "text_default", "action_button_active_border": [255, 255, 255, 100], "action_button_disabled": "background_3", "action_button_disabled_text": "text_disabled", "action_button_disabled_border": [255, 255, 255, 30], "scrollbar_background": [39, 44, 48, 0], "scrollbar_handle": [255, 255, 255, 105], "scrollbar_handle_hover": [255, 255, 255, 255], "scrollbar_handle_down": [255, 255, 255, 255], "setting_category_disabled": [75, 80, 83, 255], "setting_category_disabled_text": [255, 255, 255, 101], "setting_control": "background_2", "setting_control_selected": [34, 39, 42, 38], "setting_control_highlight": "background_3", "setting_control_border": [255, 255, 255, 38], "setting_control_border_highlight": [12, 169, 227, 255], "setting_control_text": "text_default", "setting_control_button": [255, 255, 255, 127], "setting_control_button_hover": [255, 255, 255, 204], "setting_control_disabled": [34, 39, 42, 255], "setting_control_disabled_text": [255, 255, 255, 101], "setting_control_disabled_border": [255, 255, 255, 101], "setting_unit": [255, 255, 255, 127], "setting_validation_error_background": "um_red_9", "setting_validation_warning_background": "um_yellow_9", "setting_validation_ok": "background_2", "progressbar_background": [255, 255, 255, 48], "progressbar_control": [255, 255, 255, 197], "slider_groove": [127, 127, 127, 255], "slider_groove_border": [127, 127, 127, 255], "slider_groove_fill": [245, 245, 245, 255], "slider_handle": [255, 255, 255, 255], "slider_handle_active": [68, 192, 255, 255], "category_background": "background_3", "tooltip": "background_2", "tooltip_text": "text_default", "tool_panel_background": "background_1", "viewport_background": "background_1", "volume_outline": [12, 169, 227, 128], "buildplate": [169, 169, 169, 255], "buildplate_grid_minor": [154, 154, 155, 255], "disallowed_area": [0, 0, 0, 52], "model_selection_outline": [12, 169, 227, 255], "material_compatibility_warning": [255, 255, 255, 255], "core_compatibility_warning": [255, 255, 255, 255], "quality_slider_available": [255, 255, 255, 255], "monitor_printer_family_tag": [86, 86, 106, 255], "monitor_text_disabled": [102, 102, 102, 255], "monitor_icon_primary": [229, 229, 229, 255], "monitor_icon_accent": [51, 53, 54, 255], "monitor_icon_disabled": [102, 102, 102, 255], "monitor_secondary_button_hover": [80, 80, 80, 255], "monitor_card_border": [102, 102, 102, 255], "monitor_card_background": [51, 53, 54, 255], "monitor_card_hover": [84, 89, 95, 255], "monitor_stage_background": "background_1", "monitor_stage_background_fade": "background_1", "monitor_progress_bar_deactive": [102, 102, 102, 255], "monitor_progress_bar_empty": [67, 67, 67, 255], "monitor_tooltip_text": [229, 229, 229, 255], "monitor_context_menu": [67, 67, 67, 255], "monitor_context_menu_hover": [30, 102, 215, 255], "monitor_skeleton_loading": [102, 102, 102, 255], "monitor_placeholder_image": [102, 102, 102, 255], "monitor_shadow": [4, 10, 13, 255], "monitor_carousel_dot": [119, 119, 119, 255], "monitor_carousel_dot_current": [216, 216, 216, 255]}} \ No newline at end of file +{ + "metadata": { + "name": "UltiMaker Dark", + "inherits": "cura-light" + }, + + "base_colors": + { + "background_1": [31, 31, 32, 255], + "background_2": [57, 57, 58, 255], + "background_3": [85, 85, 87, 255], + "background_4": [23, 23, 23, 255], + + "accent_1": [25, 110, 240, 255], + "accent_2": [16, 70, 156, 255], + "border_main": [118, 118, 119, 255], + "border_accent_1": [255, 255, 255, 255], + "border_accent_2": [243, 243, 243, 255], + "border_field": [57, 57, 58, 255], + + "text_default": [255, 255, 255, 255], + "text_disabled": [118, 118, 118, 255], + "text_primary_button": [255, 255, 255, 255], + "text_secondary_button": [255, 255, 255, 255], + "text_link_hover": [156, 195, 255, 255], + "text_lighter": [243, 243, 243, 255], + + "um_green_1": [233, 245, 237, 255], + "um_green_5": [36, 162, 73, 255], + "um_green_9": [31, 44, 36, 255], + "um_red_1": [251, 232, 233, 255], + "um_red_5": [218, 30, 40, 255], + "um_red_9": [59, 31, 33, 255], + "um_orange_1": [255, 235, 221, 255], + "um_orange_5": [252, 123, 30, 255], + "um_orange_9": [64, 45, 32, 255], + "um_yellow_1": [255, 248, 225, 255], + "um_yellow_5": [253, 209, 58, 255], + "um_yellow_9": [64, 58, 36, 255] + }, + + "colors": { + "main_background": "background_1", + "detail_background": "background_2", + "message_background": "background_1", + "wide_lining": [31, 36, 39, 255], + "thick_lining": [255, 255, 255, 60], + "lining": "border_main", + "viewport_overlay": "background_1", + + "primary_text": "text_default", + "secondary": [95, 95, 95, 255], + + "expandable_active": "background_2", + "expandable_hover": "background_2", + + "secondary_button": "background_1", + "secondary_button_hover": "background_3", + "secondary_button_text": [255, 255, 255, 255], + + "icon": "text_default", + "toolbar_background": "background_1", + "toolbar_button_active": "background_3", + "toolbar_button_hover": "background_3", + "toolbar_button_active_hover": "background_3", + + "main_window_header_button_background_inactive": "background_4", + "main_window_header_button_text_inactive": "text_primary_button", + "main_window_header_button_text_active": "background_4", + "main_window_header_background": "background_4", + "main_window_header_background_gradient": "background_4", + "main_window_header_button_background_hovered": [46, 46, 46, 255], + + "secondary_button_text": "text_secondary_button", + + "account_sync_state_icon": [255, 255, 255, 204], + + "machine_selector_printer_icon": [204, 204, 204, 255], + + "text": "text_default", + "text_detail": [255, 255, 255, 172], + "text_link": "accent_1", + "text_inactive": [118, 118, 118, 255], + "text_hover": [255, 255, 255, 255], + "text_scene": [250, 250, 250, 255], + "text_scene_hover": [255, 255, 255, 255], + + "printer_type_label_background": [95, 95, 95, 255], + + "error": [212, 31, 53, 255], + "disabled": [32, 32, 32, 255], + + "button": [39, 44, 48, 255], + "button_hover": [39, 44, 48, 255], + "button_text": "text_default", + "button_disabled": [39, 44, 48, 255], + "button_disabled_text": [255, 255, 255, 101], + + "small_button_text": [255, 255, 255, 197], + "small_button_text_hover": [255, 255, 255, 255], + + "button_tooltip": [39, 44, 48, 255], + + "tab_checked": [39, 44, 48, 255], + "tab_checked_border": [255, 255, 255, 30], + "tab_checked_text": [255, 255, 255, 255], + "tab_unchecked": [39, 44, 48, 255], + "tab_unchecked_border": [255, 255, 255, 30], + "tab_unchecked_text": [255, 255, 255, 101], + "tab_hovered": [39, 44, 48, 255], + "tab_hovered_border": [255, 255, 255, 30], + "tab_hovered_text": [255, 255, 255, 255], + "tab_active": [39, 44, 48, 255], + "tab_active_border": [255, 255, 255, 30], + "tab_active_text": [255, 255, 255, 255], + "tab_background": [39, 44, 48, 255], + + "action_button": "background_1", + "action_button_text": [255, 255, 255, 200], + "action_button_border": "border_main", + "action_button_hovered": [79, 85, 89, 255], + "action_button_hovered_text": "text_default", + "action_button_hovered_border": "border_main", + "action_button_active": [39, 44, 48, 30], + "action_button_active_text": "text_default", + "action_button_active_border": [255, 255, 255, 100], + "action_button_disabled": "background_3", + "action_button_disabled_text": "text_disabled", + "action_button_disabled_border": [255, 255, 255, 30], + + "scrollbar_background": [39, 44, 48, 0], + "scrollbar_handle": [255, 255, 255, 105], + "scrollbar_handle_hover": [255, 255, 255, 255], + "scrollbar_handle_down": [255, 255, 255, 255], + + "setting_category_disabled": [75, 80, 83, 255], + "setting_category_disabled_text": [255, 255, 255, 101], + + "setting_control": "background_2", + "setting_control_selected": [34, 39, 42, 38], + "setting_control_highlight": "background_3", + "setting_control_border": [255, 255, 255, 38], + "setting_control_border_highlight": [12, 169, 227, 255], + "setting_control_text": "text_default", + "setting_control_button": [255, 255, 255, 127], + "setting_control_button_hover": [255, 255, 255, 204], + "setting_control_disabled": [34, 39, 42, 255], + "setting_control_disabled_text": [255, 255, 255, 101], + "setting_control_disabled_border": [255, 255, 255, 101], + "setting_unit": [255, 255, 255, 127], + "setting_validation_error_background": "um_red_9", + "setting_validation_warning_background": "um_yellow_9", + "setting_validation_ok": "background_2", + + "progressbar_background": [255, 255, 255, 48], + "progressbar_control": [255, 255, 255, 197], + + "slider_groove": [127, 127, 127, 255], + "slider_groove_border": [127, 127, 127, 255], + "slider_groove_fill": [245, 245, 245, 255], + "slider_handle": [255, 255, 255, 255], + "slider_handle_active": [68, 192, 255, 255], + + "category_background": "background_3", + + "tooltip": "background_2", + "tooltip_text": "text_default", + + "tool_panel_background": "background_1", + + "viewport_background": "background_1", + "volume_outline": [12, 169, 227, 128], + "buildplate": [169, 169, 169, 255], + "buildplate_grid_minor": [154, 154, 155, 255], + + "disallowed_area": [0, 0, 0, 52], + + "model_selection_outline": [12, 169, 227, 255], + + "material_compatibility_warning": [255, 255, 255, 255], + + "quality_slider_available": [255, 255, 255, 255], + + "monitor_printer_family_tag": [86, 86, 106, 255], + "monitor_text_disabled": [102, 102, 102, 255], + "monitor_icon_primary": [229, 229, 229, 255], + "monitor_icon_accent": [51, 53, 54, 255], + "monitor_icon_disabled": [102, 102, 102, 255], + + "monitor_secondary_button_hover": [80, 80, 80, 255], + "monitor_card_border": [102, 102, 102, 255], + "monitor_card_background": [51, 53, 54, 255], + "monitor_card_hover": [84, 89, 95, 255], + + "monitor_stage_background": "background_1", + "monitor_stage_background_fade": "background_1", + + "monitor_progress_bar_deactive": [102, 102, 102, 255], + "monitor_progress_bar_empty": [67, 67, 67, 255], + + "monitor_tooltip_text": [229, 229, 229, 255], + "monitor_context_menu": [67, 67, 67, 255], + "monitor_context_menu_hover": [30, 102, 215, 255], + + "monitor_skeleton_loading": [102, 102, 102, 255], + "monitor_placeholder_image": [102, 102, 102, 255], + "monitor_shadow": [4, 10, 13, 255], + + "monitor_carousel_dot": [119, 119, 119, 255], + "monitor_carousel_dot_current": [216, 216, 216, 255] + } +} diff --git a/resources/themes/cura-light-colorblind/theme.json b/resources/themes/cura-light-colorblind/theme.json index cc7ed5dfba..740bf977b2 100644 --- a/resources/themes/cura-light-colorblind/theme.json +++ b/resources/themes/cura-light-colorblind/theme.json @@ -1 +1,29 @@ -{"metadata": {"name": "Colorblind Assist Light", "inherits": "cura-light"}, "colors": {"x_axis": [200, 0, 0, 255], "y_axis": [64, 64, 255, 255], "model_overhang": [200, 0, 255, 255], "model_selection_outline": [12, 169, 227, 255], "xray_error_dark": [255, 0, 0, 255], "xray_error_light": [255, 255, 0, 255], "xray": [26, 26, 62, 255], "xray_error": [255, 0, 0, 255], "layerview_inset_0": [255, 64, 0, 255], "layerview_inset_x": [0, 156, 128, 255], "layerview_skin": [255, 255, 86, 255], "layerview_support": [255, 255, 0, 255], "layerview_infill": [0, 255, 255, 255], "layerview_support_infill": [0, 200, 200, 255], "layerview_move_retraction": [0, 100, 255, 255], "main_window_header_background": [192, 199, 65, 255]}} \ No newline at end of file +{ + "metadata": { + "name": "Colorblind Assist Light", + "inherits": "cura-light" + }, + + "colors": { + + "x_axis": [200, 0, 0, 255], + "y_axis": [64, 64, 255, 255], + "model_overhang": [200, 0, 255, 255], + "model_selection_outline": [12, 169, 227, 255], + + "xray_error_dark": [255, 0, 0, 255], + "xray_error_light": [255, 255, 0, 255], + "xray": [26, 26, 62, 255], + "xray_error": [255, 0, 0, 255], + + "layerview_inset_0": [255, 64, 0, 255], + "layerview_inset_x": [0, 156, 128, 255], + "layerview_skin": [255, 255, 86, 255], + "layerview_support": [255, 255, 0, 255], + + "layerview_infill": [0, 255, 255, 255], + "layerview_support_infill": [0, 200, 200, 255], + + "layerview_move_retraction": [0, 100, 255, 255] + } +} diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 2362155944..8f3f9076c5 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -1 +1,680 @@ -{"metadata": {"name": "UltiMaker"}, "fonts": {"large": {"size": 1.35, "weight": 400, "family": "Noto Sans"}, "large_ja_JP": {"size": 1.35, "weight": 400, "family": "Noto Sans"}, "large_zh_CN": {"size": 1.35, "weight": 400, "family": "Noto Sans"}, "large_zh_TW": {"size": 1.35, "weight": 400, "family": "Noto Sans"}, "large_bold": {"size": 1.35, "weight": 600, "family": "Noto Sans"}, "huge": {"size": 1.8, "weight": 400, "family": "Noto Sans"}, "huge_bold": {"size": 1.8, "weight": 600, "family": "Noto Sans"}, "medium": {"size": 1.16, "weight": 400, "family": "Noto Sans"}, "medium_ja_JP": {"size": 1.16, "weight": 400, "family": "Noto Sans"}, "medium_zh_CN": {"size": 1.16, "weight": 400, "family": "Noto Sans"}, "medium_zh_TW": {"size": 1.16, "weight": 400, "family": "Noto Sans"}, "medium_bold": {"size": 1.16, "weight": 600, "family": "Noto Sans"}, "default": {"size": 0.95, "weight": 400, "family": "Noto Sans"}, "default_ja_JP": {"size": 1.0, "weight": 400, "family": "Noto Sans"}, "default_zh_CN": {"size": 1.0, "weight": 400, "family": "Noto Sans"}, "default_zh_TW": {"size": 1.0, "weight": 400, "family": "Noto Sans"}, "default_bold": {"size": 0.95, "weight": 600, "family": "Noto Sans"}, "default_bold_ja_JP": {"size": 1.0, "weight": 600, "family": "Noto Sans"}, "default_bold_zh_CN": {"size": 1.0, "weight": 600, "family": "Noto Sans"}, "default_bold_zh_TW": {"size": 1.0, "weight": 600, "family": "Noto Sans"}, "default_italic": {"size": 0.95, "weight": 400, "italic": true, "family": "Noto Sans"}, "medium_italic": {"size": 1.16, "weight": 400, "italic": true, "family": "Noto Sans"}, "default_italic_ja_JP": {"size": 1.0, "weight": 400, "italic": true, "family": "Noto Sans"}, "default_italic_zh_CN": {"size": 1.0, "weight": 400, "italic": true, "family": "Noto Sans"}, "default_italic_zh_TW": {"size": 1.0, "weight": 400, "italic": true, "family": "Noto Sans"}, "small": {"size": 0.9, "weight": 400, "family": "Noto Sans"}, "small_bold": {"size": 0.9, "weight": 700, "family": "Noto Sans"}, "small_ja_JP": {"size": 0.9, "weight": 400, "family": "Noto Sans"}, "small_zh_CN": {"size": 0.9, "weight": 400, "family": "Noto Sans"}, "small_zh_TW": {"size": 0.9, "weight": 400, "family": "Noto Sans"}, "small_emphasis": {"size": 0.9, "weight": 700, "family": "Noto Sans"}, "small_emphasis_ja_JP": {"size": 0.9, "weight": 700, "family": "Noto Sans"}, "small_emphasis_zh_CN": {"size": 0.9, "weight": 700, "family": "Noto Sans"}, "small_emphasis_zh_TW": {"size": 0.9, "weight": 700, "family": "Noto Sans"}, "tiny_emphasis": {"size": 0.7, "weight": 700, "family": "Noto Sans"}, "tiny_emphasis_ja_JP": {"size": 0.7, "weight": 700, "family": "Noto Sans"}, "tiny_emphasis_zh_CN": {"size": 0.7, "weight": 700, "family": "Noto Sans"}, "tiny_emphasis_zh_TW": {"size": 0.7, "weight": 700, "family": "Noto Sans"}}, "base_colors": {"background_1": [255, 255, 255, 255], "background_2": [243, 243, 243, 255], "background_3": [232, 240, 253, 255], "background_4": [3, 12, 66, 255], "accent_1": [25, 110, 240, 255], "accent_2": [16, 70, 156, 255], "border_main": [212, 212, 212, 255], "border_accent_1": [25, 110, 240, 255], "border_accent_2": [16, 70, 156, 255], "border_field": [180, 180, 180, 255], "text_default": [0, 14, 26, 255], "text_disabled": [180, 180, 180, 255], "text_primary_button": [255, 255, 255, 255], "text_secondary_button": [25, 110, 240, 255], "text_link_hover": [16, 70, 156, 255], "text_lighter": [108, 108, 108, 255], "um_green_1": [233, 245, 237, 255], "um_green_5": [36, 162, 73, 255], "um_green_9": [31, 44, 36, 255], "um_red_1": [251, 232, 233, 255], "um_red_5": [218, 30, 40, 255], "um_red_9": [59, 31, 33, 255], "um_orange_1": [255, 235, 221, 255], "um_orange_5": [252, 123, 30, 255], "um_orange_9": [64, 45, 32, 255], "um_yellow_1": [255, 248, 225, 255], "um_yellow_5": [253, 209, 58, 255], "um_yellow_9": [64, 58, 36, 255]}, "colors": {"main_background": "background_1", "detail_background": "background_2", "wide_lining": [245, 245, 245, 255], "thick_lining": [180, 180, 180, 255], "lining": [192, 193, 194, 255], "viewport_overlay": [246, 246, 246, 255], "primary": "accent_1", "primary_hover": [48, 182, 231, 255], "primary_text": [255, 255, 255, 255], "text_selection": [156, 195, 255, 127], "border": [127, 127, 127, 255], "border_field": [180, 180, 180, 255], "secondary": [240, 240, 240, 255], "expandable_active": [240, 240, 240, 255], "expandable_hover": [232, 242, 252, 255], "icon": [8, 7, 63, 255], "primary_button": "accent_1", "primary_button_hover": [16, 70, 156, 255], "primary_button_text": [255, 255, 255, 255], "secondary_button": "background_1", "secondary_button_shadow": [216, 216, 216, 255], "secondary_button_hover": [232, 240, 253, 255], "secondary_button_text": "accent_1", "main_window_header_background": [192, 199, 65, 255], "main_window_header_background_gradient": [25, 23, 91, 255], "main_window_header_button_text_active": [8, 7, 63, 255], "main_window_header_button_text_inactive": [255, 255, 255, 255], "main_window_header_button_text_hovered": [255, 255, 255, 255], "main_window_header_button_background_active": [255, 255, 255, 255], "main_window_header_button_background_inactive": [255, 255, 255, 0], "main_window_header_button_background_hovered": [117, 114, 159, 255], "account_widget_outline_active": [70, 66, 126, 255], "account_sync_state_icon": [25, 25, 25, 255], "machine_selector_printer_icon": [8, 7, 63, 255], "action_panel_secondary": "accent_1", "first_run_shadow": [50, 50, 50, 255], "toolbar_background": [255, 255, 255, 255], "notification_icon": [255, 0, 0, 255], "printer_type_label_background": [228, 228, 242, 255], "window_disabled_background": [0, 0, 0, 255], "text": [25, 25, 25, 255], "text_disabled": [180, 180, 180, 255], "text_detail": [174, 174, 174, 128], "text_link": "accent_1", "text_inactive": [174, 174, 174, 255], "text_medium": [128, 128, 128, 255], "text_scene": [102, 102, 102, 255], "text_scene_hover": [123, 123, 113, 255], "error": [218, 30, 40, 255], "warning": [253, 209, 58, 255], "success": [36, 162, 73, 255], "disabled": [229, 229, 229, 255], "toolbar_button_hover": [232, 242, 252, 255], "toolbar_button_active": [232, 242, 252, 255], "toolbar_button_active_hover": [232, 242, 252, 255], "button_text": [255, 255, 255, 255], "small_button_text": [102, 102, 102, 255], "small_button_text_hover": [8, 7, 63, 255], "button_tooltip": [31, 36, 39, 255], "extruder_disabled": [255, 255, 255, 102], "action_button": [255, 255, 255, 255], "action_button_hovered": [232, 242, 252, 255], "action_button_disabled": [245, 245, 245, 255], "action_button_disabled_text": [196, 196, 196, 255], "action_button_shadow": [223, 223, 223, 255], "scrollbar_background": [255, 255, 255, 255], "scrollbar_handle": [10, 8, 80, 255], "scrollbar_handle_hover": [50, 130, 255, 255], "scrollbar_handle_down": [50, 130, 255, 255], "setting_category": "background_1", "setting_category_disabled": [255, 255, 255, 255], "setting_category_hover": "background_2", "setting_category_text": "text_default", "setting_category_disabled_text": [24, 41, 77, 101], "setting_category_active_text": "text_default", "setting_control": "background_2", "setting_control_highlight": "background_3", "setting_control_border": [199, 199, 199, 255], "setting_control_border_highlight": [50, 130, 255, 255], "setting_control_text": [35, 35, 35, 255], "setting_control_button": [102, 102, 102, 255], "setting_control_button_hover": [8, 7, 63, 255], "setting_control_disabled": "background_2", "setting_control_disabled_text": [127, 127, 127, 255], "setting_control_disabled_border": [127, 127, 127, 255], "setting_unit": [127, 127, 127, 255], "setting_validation_error_background": "um_red_1", "setting_validation_error": "um_red_5", "setting_validation_warning_background": "um_yellow_1", "setting_validation_warning": "um_yellow_5", "setting_validation_ok": "background_2", "material_compatibility_warning": [243, 166, 59, 255], "core_compatibility_warning": [243, 166, 59, 255], "progressbar_background": [245, 245, 245, 255], "progressbar_control": [50, 130, 255, 255], "slider_groove": [223, 223, 223, 255], "slider_groove_fill": [8, 7, 63, 255], "slider_handle": [8, 7, 63, 255], "slider_handle_active": [50, 130, 255, 255], "slider_text_background": [255, 255, 255, 255], "quality_slider_unavailable": [179, 179, 179, 255], "quality_slider_available": [0, 0, 0, 255], "checkbox": "background_1", "checkbox_hover": "background_1", "checkbox_disabled": "background_2", "checkbox_border": [180, 180, 180, 255], "checkbox_border_hover": "border_main", "checkbox_border_disabled": "text_disabled", "checkbox_mark": "text_default", "checkbox_mark_disabled": "text_disabled", "checkbox_square": [180, 180, 180, 255], "checkbox_text": "text_default", "checkbox_text_disabled": "text_disabled", "switch": "background_1", "switch_state_checked": "accent_1", "switch_state_unchecked": "text_disabled", "radio": "background_1", "radio_disabled": "background_2", "radio_selected": "accent_1", "radio_selected_disabled": "text_disabled", "radio_border": [180, 180, 180, 255], "radio_border_hover": "border_main", "radio_border_disabled": "text_disabled", "radio_dot": "background_1", "radio_dot_disabled": "background_2", "radio_text": "text_default", "radio_text_disabled": "text_disabled", "text_field": "background_1", "text_field_border": [180, 180, 180, 255], "text_field_border_hovered": "border_main", "text_field_border_active": "border_accent_2", "text_field_border_disabled": "background_2", "text_field_text": "text_default", "text_field_text_disabled": "text_disabled", "category_background": "background_2", "tooltip": [25, 25, 25, 255], "tooltip_text": [255, 255, 255, 255], "message_background": [255, 255, 255, 255], "message_border": [192, 193, 194, 255], "message_close": [102, 102, 102, 255], "message_close_hover": [8, 7, 63, 255], "message_progressbar_background": [245, 245, 245, 255], "message_progressbar_control": [50, 130, 255, 255], "message_success_icon": [255, 255, 255, 255], "message_warning_icon": [0, 0, 0, 255], "message_error_icon": [255, 255, 255, 255], "tool_panel_background": [255, 255, 255, 255], "status_offline": [0, 0, 0, 255], "status_ready": [0, 205, 0, 255], "status_busy": [50, 130, 255, 255], "status_paused": [255, 140, 0, 255], "status_stopped": [236, 82, 80, 255], "disabled_axis": [127, 127, 127, 255], "x_axis": [218, 30, 40, 255], "y_axis": [25, 110, 240, 255], "z_axis": [36, 162, 73, 255], "all_axis": [255, 255, 255, 255], "viewport_background": [250, 250, 250, 255], "volume_outline": [50, 130, 255, 255], "buildplate": [244, 244, 244, 255], "buildplate_grid": [180, 180, 180, 255], "buildplate_grid_minor": [228, 228, 228, 255], "convex_hull": [35, 35, 35, 127], "disallowed_area": [0, 0, 0, 40], "error_area": [255, 0, 0, 127], "model_overhang": [255, 0, 0, 255], "model_unslicable": [122, 122, 122, 255], "model_unslicable_alt": [172, 172, 127, 255], "model_selection_outline": [50, 130, 255, 255], "model_non_printing": [122, 122, 122, 255], "xray": [26, 26, 62, 255], "layerview_ghost": [31, 31, 31, 95], "layerview_none": [255, 255, 255, 255], "layerview_inset_0": [230, 0, 0, 255], "layerview_inset_x": [0, 230, 0, 255], "layerview_skin": [230, 230, 0, 255], "layerview_support": [0, 230, 230, 127], "layerview_skirt": [0, 230, 230, 255], "layerview_infill": [230, 115, 0, 255], "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], "layerview_starts": [255, 255, 255, 255], "monitor_printer_family_tag": [228, 228, 242, 255], "monitor_text_disabled": [238, 238, 238, 255], "monitor_icon_primary": [10, 8, 80, 255], "monitor_icon_accent": [255, 255, 255, 255], "monitor_icon_disabled": [238, 238, 238, 255], "monitor_card_border": [192, 193, 194, 255], "monitor_card_background": [255, 255, 255, 255], "monitor_card_hover": [232, 242, 252, 255], "monitor_stage_background": [246, 246, 246, 255], "monitor_stage_background_fade": [246, 246, 246, 102], "monitor_tooltip": [25, 25, 25, 255], "monitor_tooltip_text": [255, 255, 255, 255], "monitor_context_menu": [255, 255, 255, 255], "monitor_context_menu_hover": [245, 245, 245, 255], "monitor_skeleton_loading": [238, 238, 238, 255], "monitor_placeholder_image": [230, 230, 230, 255], "monitor_image_overlay": [0, 0, 0, 255], "monitor_shadow": [200, 200, 200, 255], "monitor_carousel_dot": [216, 216, 216, 255], "monitor_carousel_dot_current": [119, 119, 119, 255], "cloud_unavailable": [153, 153, 153, 255], "connection_badge_background": [255, 255, 255, 255], "warning_badge_background": [0, 0, 0, 255], "error_badge_background": [255, 255, 255, 255], "border_field_light": [180, 180, 180, 255], "border_main_light": [212, 212, 212, 255]}, "sizes": {"window_minimum_size": [80, 48], "popup_dialog": [40, 36], "small_popup_dialog": [36, 12], "main_window_header": [0.0, 4.0], "stage_menu": [0.0, 4.0], "account_button": [12, 2.5], "print_setup_widget": [38.0, 30.0], "print_setup_extruder_box": [0.0, 6.0], "slider_widget_groove": [0.16, 0.16], "slider_widget_handle": [1.3, 1.3], "slider_widget_tickmarks": [0.5, 0.5], "print_setup_big_item": [28, 2.5], "print_setup_icon": [1.2, 1.2], "drag_icon": [1.416, 0.25], "application_switcher_item": [8, 9], "application_switcher_icon": [3.75, 3.75], "expandable_component_content_header": [0.0, 3.0], "configuration_selector": [35.0, 4.0], "action_panel_widget": [26.0, 0.0], "action_panel_information_widget": [20.0, 0.0], "machine_selector_widget": [20.0, 4.0], "machine_selector_widget_content": [25.0, 32.0], "machine_selector_icon": [2.5, 2.5], "views_selector": [16.0, 4.0], "printer_type_label": [3.5, 1.5], "default_radius": [0.25, 0.25], "wide_lining": [0.5, 0.5], "thick_lining": [0.2, 0.2], "default_lining": [0.08, 0.08], "default_arrow": [0.8, 0.8], "logo": [16, 2], "wide_margin": [2.0, 2.0], "thick_margin": [1.71, 1.43], "default_margin": [1.0, 1.0], "thin_margin": [0.71, 0.71], "narrow_margin": [0.5, 0.5], "extruder_icon": [2.5, 2.5], "section": [0.0, 2], "section_header": [0.0, 2.5], "section_control": [0, 1], "section_icon": [1.5, 1.5], "section_icon_column": [2.5, 2.5], "setting": [25.0, 1.8], "setting_control": [9.0, 2.0], "setting_control_radius": [0.15, 0.15], "setting_control_depth_margin": [1.4, 0.0], "setting_unit_margin": [0.5, 0.5], "standard_list_lineheight": [1.5, 1.5], "standard_arrow": [1.0, 1.0], "card": [25.0, 10], "card_icon": [6.0, 6.0], "card_tiny_icon": [1.5, 1.5], "button": [4, 4], "button_icon": [2.5, 2.5], "action_button": [15.0, 2.5], "action_button_icon": [1.5, 1.5], "action_button_icon_small": [1.0, 1.0], "action_button_radius": [0.15, 0.15], "radio_button": [1.3, 1.3], "small_button": [2, 2], "small_button_icon": [1.5, 1.5], "medium_button": [2.5, 2.5], "medium_button_icon": [2, 2], "large_button": [3.0, 3.0], "large_button_icon": [2.8, 2.8], "context_menu": [20, 2], "icon_indicator": [1, 1], "printer_status_icon": [1.0, 1.0], "button_tooltip": [1.0, 1.3], "button_tooltip_arrow": [0.25, 0.25], "progressbar": [26.0, 0.75], "progressbar_radius": [0.15, 0.15], "scrollbar": [0.75, 0.5], "slider_groove": [0.5, 0.5], "slider_groove_radius": [0.15, 0.15], "slider_handle": [1.5, 1.5], "slider_layerview_size": [1.0, 34.0], "layerview_menu_size": [16.0, 4.0], "layerview_legend_size": [1.0, 1.0], "layerview_row": [11.0, 1.5], "layerview_row_spacing": [0.0, 0.5], "checkbox": [1.33, 1.33], "checkbox_mark": [1, 1], "checkbox_radius": [0.25, 0.25], "spinbox": [6.0, 3.0], "combobox": [14, 2], "combobox_wide": [22, 2], "tooltip": [20.0, 10.0], "tooltip_margins": [1.0, 1.0], "tooltip_arrow_margins": [2.0, 2.0], "save_button_save_to_button": [0.3, 2.7], "save_button_specs_icons": [1.4, 1.4], "first_run_shadow_radius": [1.2, 1.2], "monitor_preheat_temperature_control": [4.5, 2.0], "welcome_wizard_window": [46, 50], "modal_window_minimum": [60.0, 50.0], "wizard_progress": [10.0, 0.0], "message": [30.0, 5.0], "message_close": [2, 2], "message_radius": [0.25, 0.25], "message_action_button": [0, 2.5], "message_image": [15.0, 10.0], "message_type_icon": [2, 2], "menu": [18, 2], "jobspecs_line": [2.0, 2.0], "objects_menu_size": [15, 15], "notification_icon": [1.5, 1.5], "avatar_image": [6.8, 6.8], "monitor_shadow_radius": [0.4, 0.4], "monitor_empty_state_offset": [5.6, 5.6], "monitor_empty_state_size": [35.0, 25.0], "monitor_column": [18.0, 1.0], "monitor_progress_bar": [16.5, 1.0], "table_row": [2.0, 2.0], "welcome_wizard_content_image_big": [18, 15], "welcome_wizard_cloud_content_image": [4, 4], "banner_icon_size": [2.0, 2.0], "marketplace_large_icon": [4.0, 4.0], "preferences_page_list_item": [8.0, 2.0], "recommended_button_icon": [1.7, 1.7], "recommended_section_setting_item": [14.0, 2.0], "reset_profile_icon": [1, 1]}} \ No newline at end of file +{ + "metadata": { + "name": "UltiMaker" + }, + + "fonts": { + "large": { + "size": 1.35, + "weight": 400, + "family": "Noto Sans" + }, + "large_ja_JP": { + "size": 1.35, + "weight": 400, + "family": "Noto Sans" + }, + "large_zh_CN": { + "size": 1.35, + "weight": 400, + "family": "Noto Sans" + }, + "large_zh_TW": { + "size": 1.35, + "weight": 400, + "family": "Noto Sans" + }, + "large_bold": { + "size": 1.35, + "weight": 600, + "family": "Noto Sans" + }, + "huge": { + "size": 1.8, + "weight": 400, + "family": "Noto Sans" + }, + "huge_bold": { + "size": 1.8, + "weight": 600, + "family": "Noto Sans" + }, + "medium": { + "size": 1.16, + "weight": 400, + "family": "Noto Sans" + }, + "medium_ja_JP": { + "size": 1.16, + "weight": 400, + "family": "Noto Sans" + }, + "medium_zh_CN": { + "size": 1.16, + "weight": 400, + "family": "Noto Sans" + }, + "medium_zh_TW": { + "size": 1.16, + "weight": 400, + "family": "Noto Sans" + }, + "medium_bold": { + "size": 1.16, + "weight": 600, + "family": "Noto Sans" + }, + "default": { + "size": 0.95, + "weight": 400, + "family": "Noto Sans" + }, + "default_ja_JP": { + "size": 1.0, + "weight": 400, + "family": "Noto Sans" + }, + "default_zh_CN": { + "size": 1.0, + "weight": 400, + "family": "Noto Sans" + }, + "default_zh_TW": { + "size": 1.0, + "weight": 400, + "family": "Noto Sans" + }, + "default_bold": { + "size": 0.95, + "weight": 600, + "family": "Noto Sans" + }, + "default_bold_ja_JP": { + "size": 1.0, + "weight": 600, + "family": "Noto Sans" + }, + "default_bold_zh_CN": { + "size": 1.0, + "weight": 600, + "family": "Noto Sans" + }, + "default_bold_zh_TW": { + "size": 1.0, + "weight": 600, + "family": "Noto Sans" + }, + "default_italic": { + "size": 0.95, + "weight": 400, + "italic": true, + "family": "Noto Sans" + }, + "default_italic_ja_JP": { + "size": 1.0, + "weight": 400, + "italic": true, + "family": "Noto Sans" + }, + "default_italic_zh_CN": { + "size": 1.0, + "weight": 400, + "italic": true, + "family": "Noto Sans" + }, + "default_italic_zh_TW": { + "size": 1.0, + "weight": 400, + "italic": true, + "family": "Noto Sans" + }, + "small": { + "size": 0.9, + "weight": 400, + "family": "Noto Sans" + }, + "small_bold": { + "size": 0.9, + "weight": 700, + "family": "Noto Sans" + }, + "small_ja_JP": { + "size": 0.9, + "weight": 400, + "family": "Noto Sans" + }, + "small_zh_CN": { + "size": 0.9, + "weight": 400, + "family": "Noto Sans" + }, + "small_zh_TW": { + "size": 0.9, + "weight": 400, + "family": "Noto Sans" + }, + "small_emphasis": { + "size": 0.9, + "weight": 700, + "family": "Noto Sans" + }, + "small_emphasis_ja_JP": { + "size": 0.9, + "weight": 700, + "family": "Noto Sans" + }, + "small_emphasis_zh_CN": { + "size": 0.9, + "weight": 700, + "family": "Noto Sans" + }, + "small_emphasis_zh_TW": { + "size": 0.9, + "weight": 700, + "family": "Noto Sans" + }, + "tiny_emphasis": { + "size": 0.7, + "weight": 700, + "family": "Noto Sans" + }, + "tiny_emphasis_ja_JP": { + "size": 0.7, + "weight": 700, + "family": "Noto Sans" + }, + "tiny_emphasis_zh_CN": { + "size": 0.7, + "weight": 700, + "family": "Noto Sans" + }, + "tiny_emphasis_zh_TW": { + "size": 0.7, + "weight": 700, + "family": "Noto Sans" + } + }, + + "base_colors": { + "background_1": [255, 255, 255, 255], + "background_2": [243, 243, 243, 255], + "background_3": [232, 240, 253, 255], + "background_4": [3, 12, 66, 255], + + "accent_1": [25, 110, 240, 255], + "accent_2": [16, 70, 156, 255], + "border_main": [212, 212, 212, 255], + "border_accent_1": [25, 110, 240, 255], + "border_accent_2": [16, 70, 156, 255], + "border_field": [180, 180, 180, 255], + + "text_default": [0, 14, 26, 255], + "text_disabled": [180, 180, 180, 255], + "text_primary_button": [255, 255, 255, 255], + "text_secondary_button": [25, 110, 240, 255], + "text_link_hover": [16, 70, 156, 255], + "text_lighter": [108, 108, 108, 255], + + "um_green_1": [233, 245, 237, 255], + "um_green_5": [36, 162, 73, 255], + "um_green_9": [31, 44, 36, 255], + "um_red_1": [251, 232, 233, 255], + "um_red_5": [218, 30, 40, 255], + "um_red_9": [59, 31, 33, 255], + "um_orange_1": [255, 235, 221, 255], + "um_orange_5": [252, 123, 30, 255], + "um_orange_9": [64, 45, 32, 255], + "um_yellow_1": [255, 248, 225, 255], + "um_yellow_5": [253, 209, 58, 255], + "um_yellow_9": [64, 58, 36, 255] + }, + + "colors": { + + "main_background": "background_1", + "detail_background": "background_2", + "wide_lining": [245, 245, 245, 255], + "thick_lining": [180, 180, 180, 255], + "lining": [192, 193, 194, 255], + "viewport_overlay": [246, 246, 246, 255], + + "primary": "accent_1", + "primary_hover": [48, 182, 231, 255], + "primary_text": [255, 255, 255, 255], + "text_selection": [156, 195, 255, 127], + "border": [127, 127, 127, 255], + "border_field": [180, 180, 180, 255], + "secondary": [240, 240, 240, 255], + + "expandable_active": [240, 240, 240, 255], + "expandable_hover": [232, 242, 252, 255], + + "icon": [8, 7, 63, 255], + + "primary_button": "accent_1", + "primary_button_hover": [16, 70, 156, 255], + "primary_button_text": [255, 255, 255, 255], + + "secondary_button": "background_1", + "secondary_button_shadow": [216, 216, 216, 255], + "secondary_button_hover": [232, 240, 253, 255], + "secondary_button_text": "accent_1", + + "main_window_header_background": [8, 7, 63, 255], + "main_window_header_background_gradient": [25, 23, 91, 255], + "main_window_header_button_text_active": [8, 7, 63, 255], + "main_window_header_button_text_inactive": [255, 255, 255, 255], + "main_window_header_button_text_hovered": [255, 255, 255, 255], + "main_window_header_button_background_active": [255, 255, 255, 255], + "main_window_header_button_background_inactive": [255, 255, 255, 0], + "main_window_header_button_background_hovered": [117, 114, 159, 255], + + "account_widget_outline_active": [70, 66, 126, 255], + "account_sync_state_icon": [25, 25, 25, 255], + + "machine_selector_printer_icon": [8, 7, 63, 255], + + "action_panel_secondary": "accent_1", + + "first_run_shadow": [50, 50, 50, 255], + + "toolbar_background": [255, 255, 255, 255], + + "notification_icon": [255, 0, 0, 255], + + "printer_type_label_background": [228, 228, 242, 255], + + "window_disabled_background": [0, 0, 0, 255], + + "text": [25, 25, 25, 255], + "text_disabled": [180, 180, 180, 255], + "text_detail": [174, 174, 174, 128], + "text_link": "accent_1", + "text_inactive": [174, 174, 174, 255], + "text_medium": [128, 128, 128, 255], + "text_scene": [102, 102, 102, 255], + "text_scene_hover": [123, 123, 113, 255], + + "error": [218, 30, 40, 255], + "warning": [253, 209, 58, 255], + "success": [36, 162, 73, 255], + "disabled": [229, 229, 229, 255], + + "toolbar_button_hover": [232, 242, 252, 255], + "toolbar_button_active": [232, 242, 252, 255], + "toolbar_button_active_hover": [232, 242, 252, 255], + + "button_text": [255, 255, 255, 255], + + "small_button_text": [102, 102, 102, 255], + "small_button_text_hover": [8, 7, 63, 255], + + "button_tooltip": [31, 36, 39, 255], + + "extruder_disabled": [255, 255, 255, 102], + + "action_button": [255, 255, 255, 255], + "action_button_hovered": [232, 242, 252, 255], + "action_button_disabled": [245, 245, 245, 255], + "action_button_disabled_text": [196, 196, 196, 255], + "action_button_shadow": [223, 223, 223, 255], + + "scrollbar_background": [255, 255, 255, 255], + "scrollbar_handle": [10, 8, 80, 255], + "scrollbar_handle_hover": [50, 130, 255, 255], + "scrollbar_handle_down": [50, 130, 255, 255], + + "setting_category": "background_1", + "setting_category_disabled": [255, 255, 255, 255], + "setting_category_hover": "background_2", + "setting_category_text": "text_default", + "setting_category_disabled_text": [24, 41, 77, 101], + "setting_category_active_text": "text_default", + + "setting_control": "background_2", + "setting_control_highlight": "background_3", + "setting_control_border": [199, 199, 199, 255], + "setting_control_border_highlight": [50, 130, 255, 255], + "setting_control_text": [35, 35, 35, 255], + "setting_control_button": [102, 102, 102, 255], + "setting_control_button_hover": [8, 7, 63, 255], + "setting_control_disabled": "background_2", + "setting_control_disabled_text": [127, 127, 127, 255], + "setting_control_disabled_border": [127, 127, 127, 255], + "setting_unit": [127, 127, 127, 255], + "setting_validation_error_background": "um_red_1", + "setting_validation_error": "um_red_5", + "setting_validation_warning_background": "um_yellow_1", + "setting_validation_warning": "um_yellow_5", + "setting_validation_ok": "background_2", + + "material_compatibility_warning": [243, 166, 59, 255], + + "progressbar_background": [245, 245, 245, 255], + "progressbar_control": [50, 130, 255, 255], + + "slider_groove": [223, 223, 223, 255], + "slider_groove_fill": [8, 7, 63, 255], + "slider_handle": [8, 7, 63, 255], + "slider_handle_active": [50, 130, 255, 255], + "slider_text_background": [255, 255, 255, 255], + + "quality_slider_unavailable": [179, 179, 179, 255], + "quality_slider_available": [0, 0, 0, 255], + + "checkbox": "background_1", + "checkbox_hover": "background_1", + "checkbox_disabled": "background_2", + "checkbox_border": [180, 180, 180, 255], + "checkbox_border_hover": "border_main", + "checkbox_border_disabled": "text_disabled", + "checkbox_mark": "text_default", + "checkbox_mark_disabled": "text_disabled", + "checkbox_square": [180, 180, 180, 255], + "checkbox_text": "text_default", + "checkbox_text_disabled": "text_disabled", + + "switch": "background_1", + "switch_state_checked": "accent_1", + "switch_state_unchecked": "text_disabled", + + "radio": "background_1", + "radio_disabled": "background_2", + "radio_selected": "accent_1", + "radio_selected_disabled": "text_disabled", + "radio_border": [180, 180, 180, 255], + "radio_border_hover": "border_main", + "radio_border_disabled": "text_disabled", + "radio_dot": "background_1", + "radio_dot_disabled": "background_2", + "radio_text": "text_default", + "radio_text_disabled": "text_disabled", + + "text_field": "background_1", + "text_field_border": [180, 180, 180, 255], + "text_field_border_hovered": "border_main", + "text_field_border_active": "border_accent_2", + "text_field_border_disabled": "background_2", + "text_field_text": "text_default", + "text_field_text_disabled": "text_disabled", + + "category_background": "background_2", + + "tooltip": [25, 25, 25, 255], + "tooltip_text": [255, 255, 255, 255], + + "message_background": [255, 255, 255, 255], + "message_border": [192, 193, 194, 255], + "message_close": [102, 102, 102, 255], + "message_close_hover": [8, 7, 63, 255], + "message_progressbar_background": [245, 245, 245, 255], + "message_progressbar_control": [50, 130, 255, 255], + "message_success_icon": [255, 255, 255, 255], + "message_warning_icon": [0, 0, 0, 255], + "message_error_icon": [255, 255, 255, 255], + + "tool_panel_background": [255, 255, 255, 255], + + "status_offline": [0, 0, 0, 255], + "status_ready": [0, 205, 0, 255], + "status_busy": [50, 130, 255, 255], + "status_paused": [255, 140, 0, 255], + "status_stopped": [236, 82, 80, 255], + + "disabled_axis": [127, 127, 127, 255], + "x_axis": [218, 30, 40, 255], + "y_axis": [25, 110, 240, 255], + "z_axis": [36, 162, 73, 255], + "all_axis": [255, 255, 255, 255], + + "viewport_background": [250, 250, 250, 255], + "volume_outline": [50, 130, 255, 255], + "buildplate": [244, 244, 244, 255], + "buildplate_grid": [180, 180, 180, 255], + "buildplate_grid_minor": [228, 228, 228, 255], + + "convex_hull": [35, 35, 35, 127], + "disallowed_area": [0, 0, 0, 40], + "error_area": [255, 0, 0, 127], + + "model_overhang": [255, 0, 0, 255], + "model_unslicable": [122, 122, 122, 255], + "model_unslicable_alt": [172, 172, 127, 255], + "model_selection_outline": [50, 130, 255, 255], + "model_non_printing": [122, 122, 122, 255], + + "xray": [26, 26, 62, 255], + + "layerview_ghost": [31, 31, 31, 95], + "layerview_none": [255, 255, 255, 255], + "layerview_inset_0": [230, 0, 0, 255], + "layerview_inset_x": [0, 230, 0, 255], + "layerview_skin": [230, 230, 0, 255], + "layerview_support": [0, 230, 230, 127], + "layerview_skirt": [0, 230, 230, 255], + "layerview_infill": [230, 115, 0, 255], + "layerview_support_infill": [0, 230, 230, 127], + "layerview_move_combing": [0, 0, 255, 255], + "layerview_move_retraction": [128, 127, 255, 255], + "layerview_support_interface": [63, 127, 255, 127], + "layerview_prime_tower": [0, 255, 255, 255], + "layerview_nozzle": [224, 192, 16, 64], + "layerview_starts": [255, 255, 255, 255], + + + "monitor_printer_family_tag": [228, 228, 242, 255], + "monitor_text_disabled": [238, 238, 238, 255], + "monitor_icon_primary": [10, 8, 80, 255], + "monitor_icon_accent": [255, 255, 255, 255], + "monitor_icon_disabled": [238, 238, 238, 255], + + "monitor_card_border": [192, 193, 194, 255], + "monitor_card_background": [255, 255, 255, 255], + "monitor_card_hover": [232, 242, 252, 255], + + "monitor_stage_background": [246, 246, 246, 255], + "monitor_stage_background_fade": [246, 246, 246, 102], + + "monitor_tooltip": [25, 25, 25, 255], + "monitor_tooltip_text": [255, 255, 255, 255], + "monitor_context_menu": [255, 255, 255, 255], + "monitor_context_menu_hover": [245, 245, 245, 255], + + "monitor_skeleton_loading": [238, 238, 238, 255], + "monitor_placeholder_image": [230, 230, 230, 255], + "monitor_image_overlay": [0, 0, 0, 255], + "monitor_shadow": [200, 200, 200, 255], + + "monitor_carousel_dot": [216, 216, 216, 255], + "monitor_carousel_dot_current": [119, 119, 119, 255], + + "cloud_unavailable": [153, 153, 153, 255], + "connection_badge_background": [255, 255, 255, 255], + "warning_badge_background": [0, 0, 0, 255], + "error_badge_background": [255, 255, 255, 255], + + "border_field_light": [180, 180, 180, 255], + "border_main_light": [212, 212, 212, 255] + }, + + "sizes": { + "window_minimum_size": [80, 48], + "popup_dialog": [40, 36], + "small_popup_dialog": [36, 12], + + "main_window_header": [0.0, 4.0], + + "stage_menu": [0.0, 4.0], + + "account_button": [12, 2.5], + + "print_setup_widget": [38.0, 30.0], + "print_setup_extruder_box": [0.0, 6.0], + "slider_widget_groove": [0.16, 0.16], + "slider_widget_handle": [1.3, 1.3], + "slider_widget_tickmarks": [0.5, 0.5], + "print_setup_big_item": [28, 2.5], + "print_setup_icon": [1.2, 1.2], + "drag_icon": [1.416, 0.25], + + "application_switcher_item": [8, 9], + "application_switcher_icon": [3.75, 3.75], + + "expandable_component_content_header": [0.0, 3.0], + + "configuration_selector": [35.0, 4.0], + + "action_panel_widget": [26.0, 0.0], + "action_panel_information_widget": [20.0, 0.0], + + "machine_selector_widget": [20.0, 4.0], + "machine_selector_widget_content": [25.0, 32.0], + "machine_selector_icon": [2.5, 2.5], + + "views_selector": [16.0, 4.0], + + "printer_type_label": [3.5, 1.5], + + "default_radius": [0.25, 0.25], + + "wide_lining": [0.5, 0.5], + "thick_lining": [0.2, 0.2], + "default_lining": [0.08, 0.08], + + "default_arrow": [0.8, 0.8], + "logo": [16, 2], + + "wide_margin": [2.0, 2.0], + "thick_margin": [1.71, 1.43], + "default_margin": [1.0, 1.0], + "thin_margin": [0.71, 0.71], + "narrow_margin": [0.5, 0.5], + + "extruder_icon": [2.5, 2.5], + + "section": [0.0, 2], + "section_header": [0.0, 2.5], + + "section_control": [0, 1], + "section_icon": [1.5, 1.5], + "section_icon_column": [2.5, 2.5], + + "setting": [25.0, 1.8], + "setting_control": [11.0, 2.0], + "setting_control_radius": [0.15, 0.15], + "setting_control_depth_margin": [1.4, 0.0], + "setting_unit_margin": [0.5, 0.5], + + "standard_list_lineheight": [1.5, 1.5], + "standard_arrow": [1.0, 1.0], + + "card": [25.0, 10], + "card_icon": [6.0, 6.0], + "card_tiny_icon": [1.5, 1.5], + + "button": [4, 4], + "button_icon": [2.5, 2.5], + + "action_button": [15.0, 2.5], + "action_button_icon": [1.5, 1.5], + "action_button_icon_small": [1.0, 1.0], + "action_button_radius": [0.15, 0.15], + + "radio_button": [1.3, 1.3], + + "small_button": [2, 2], + "small_button_icon": [1.5, 1.5], + + "medium_button": [2.5, 2.5], + "medium_button_icon": [2, 2], + + "large_button": [3.0, 3.0], + "large_button_icon": [2.8, 2.8], + + "context_menu": [20, 2], + + "icon_indicator": [1, 1], + + "printer_status_icon": [1.0, 1.0], + + "button_tooltip": [1.0, 1.3], + "button_tooltip_arrow": [0.25, 0.25], + + "progressbar": [26.0, 0.75], + "progressbar_radius": [0.15, 0.15], + + "scrollbar": [0.75, 0.5], + + "slider_groove": [0.5, 0.5], + "slider_groove_radius": [0.15, 0.15], + "slider_handle": [1.5, 1.5], + "slider_layerview_size": [1.0, 34.0], + + "layerview_menu_size": [16.0, 4.0], + "layerview_legend_size": [1.0, 1.0], + "layerview_row": [11.0, 1.5], + "layerview_row_spacing": [0.0, 0.5], + + "checkbox": [1.33, 1.33], + "checkbox_mark": [1, 1], + "checkbox_radius": [0.25, 0.25], + + "spinbox": [6.0, 3.0], + "combobox": [14, 2], + "combobox_wide": [22, 2], + + "tooltip": [20.0, 10.0], + "tooltip_margins": [1.0, 1.0], + "tooltip_arrow_margins": [2.0, 2.0], + + "save_button_save_to_button": [0.3, 2.7], + "save_button_specs_icons": [1.4, 1.4], + + "first_run_shadow_radius": [1.2, 1.2], + + "monitor_preheat_temperature_control": [4.5, 2.0], + + "welcome_wizard_window": [46, 50], + "modal_window_minimum": [60.0, 45], + "wizard_progress": [10.0, 0.0], + + "message": [30.0, 5.0], + "message_close": [1, 1], + "message_radius": [0.25, 0.25], + "message_action_button": [0, 2.5], + "message_image": [15.0, 10.0], + "message_type_icon": [2, 2], + "menu": [18, 2], + + "jobspecs_line": [2.0, 2.0], + + "objects_menu_size": [15, 15], + + "notification_icon": [1.5, 1.5], + + "avatar_image": [6.8, 6.8], + + "monitor_shadow_radius": [0.4, 0.4], + "monitor_empty_state_offset": [5.6, 5.6], + "monitor_empty_state_size": [35.0, 25.0], + "monitor_column": [18.0, 1.0], + "monitor_progress_bar": [16.5, 1.0], + + "table_row": [2.0, 2.0], + + "welcome_wizard_content_image_big": [18, 15], + "welcome_wizard_cloud_content_image": [4, 4], + + "banner_icon_size": [2.0, 2.0], + + "marketplace_large_icon": [4.0, 4.0], + + "preferences_page_list_item": [8.0, 2.0], + + "recommended_button_icon": [1.7, 1.7], + + "recommended_section_setting_item": [14.0, 2.0], + + "reset_profile_icon": [1, 1] + } +} diff --git a/resources/themes/daily_test_colors.json b/resources/themes/daily_test_colors.json new file mode 100644 index 0000000000..1cfa2baa74 --- /dev/null +++ b/resources/themes/daily_test_colors.json @@ -0,0 +1,16 @@ +[ + [ 62, 33, 55, 255], + [126, 196, 193, 255], + [126, 196, 193, 255], + [215, 155, 125, 255], + [228, 148, 58, 255], + [192, 199, 65, 255], + [157, 48, 59, 255], + [140, 143, 174, 255], + [ 23, 67, 75, 255], + [ 23, 67, 75, 255], + [154, 99, 72, 255], + [112, 55, 127, 255], + [100, 125, 52, 255], + [210, 100, 113, 255] +] From 851d472aafe6395c87c07e73e7cf6d2809ec0e8e Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 17 Jun 2025 10:12:52 +0200 Subject: [PATCH 040/159] Restore original themes --- resources/themes/cura-dark/theme.json | 8 ++++---- resources/themes/cura-light/theme.json | 13 ++++++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/resources/themes/cura-dark/theme.json b/resources/themes/cura-dark/theme.json index 64c3e002a9..1517b22eb9 100644 --- a/resources/themes/cura-dark/theme.json +++ b/resources/themes/cura-dark/theme.json @@ -56,7 +56,7 @@ "secondary_button": "background_1", "secondary_button_hover": "background_3", - "secondary_button_text": [255, 255, 255, 255], + "secondary_button_text": "text_secondary_button", "icon": "text_default", "toolbar_background": "background_1", @@ -69,9 +69,7 @@ "main_window_header_button_text_active": "background_4", "main_window_header_background": "background_4", "main_window_header_background_gradient": "background_4", - "main_window_header_button_background_hovered": [46, 46, 46, 255], - - "secondary_button_text": "text_secondary_button", + "main_window_header_button_background_hovered": [46, 46, 46, 255], "account_sync_state_icon": [255, 255, 255, 204], @@ -179,6 +177,8 @@ "material_compatibility_warning": [255, 255, 255, 255], + "core_compatibility_warning": [255, 255, 255, 255], + "quality_slider_available": [255, 255, 255, 255], "monitor_printer_family_tag": [86, 86, 106, 255], diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 8f3f9076c5..1ae316f96c 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -110,6 +110,12 @@ "italic": true, "family": "Noto Sans" }, + "medium_italic": { + "size": 1.16, + "weight": 400, + "italic": true, + "family": "Noto Sans" + }, "default_italic_ja_JP": { "size": 1.0, "weight": 400, @@ -349,6 +355,7 @@ "setting_validation_ok": "background_2", "material_compatibility_warning": [243, 166, 59, 255], + "core_compatibility_warning": [243, 166, 59, 255], "progressbar_background": [245, 245, 245, 255], "progressbar_control": [50, 130, 255, 255], @@ -560,7 +567,7 @@ "section_icon_column": [2.5, 2.5], "setting": [25.0, 1.8], - "setting_control": [11.0, 2.0], + "setting_control": [9.0, 2.0], "setting_control_radius": [0.15, 0.15], "setting_control_depth_margin": [1.4, 0.0], "setting_unit_margin": [0.5, 0.5], @@ -635,11 +642,11 @@ "monitor_preheat_temperature_control": [4.5, 2.0], "welcome_wizard_window": [46, 50], - "modal_window_minimum": [60.0, 45], + "modal_window_minimum": [60.0, 50.0], "wizard_progress": [10.0, 0.0], "message": [30.0, 5.0], - "message_close": [1, 1], + "message_close": [2, 2], "message_radius": [0.25, 0.25], "message_action_button": [0, 2.5], "message_image": [15.0, 10.0], From 6713ec3d7397bb22629ecfcd34ce0cf74403f6d4 Mon Sep 17 00:00:00 2001 From: Frederic Meeuwissen <13856291+Frederic98@users.noreply.github.com> Date: Tue, 17 Jun 2025 13:29:29 +0200 Subject: [PATCH 041/159] [PP-605] Add missing machine_nozzle_size to S6 BB04 --- resources/variants/ultimaker_s6_bb04.inst.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/variants/ultimaker_s6_bb04.inst.cfg b/resources/variants/ultimaker_s6_bb04.inst.cfg index 756d6fd1d4..e0c62d9596 100644 --- a/resources/variants/ultimaker_s6_bb04.inst.cfg +++ b/resources/variants/ultimaker_s6_bb04.inst.cfg @@ -11,6 +11,7 @@ type = variant [values] machine_nozzle_heat_up_speed = 1.5 machine_nozzle_id = BB 0.4 +machine_nozzle_size = 0.4 machine_nozzle_tip_outer_diameter = 1.0 retraction_amount = 4.5 support_bottom_height = =layer_height * 2 From 265599a52ded3724c4d0656276156841ed200daa Mon Sep 17 00:00:00 2001 From: Sam Bonnekamp Date: Wed, 18 Jun 2025 01:31:17 +0800 Subject: [PATCH 042/159] add Anycubic kobra 3 v2 (and ace pro) profiles --- .../definitions/anycubic_kobra3v2.def.json | 52 +++++++++++++ .../anycubic_kobra3v2_ACE_PRO.def.json | 69 ++++++++++++++++++ ...ycubic_kobra3v2_ACEPRO_extruder_0.def.json | 16 ++++ ...ycubic_kobra3v2_ACEPRO_extruder_1.def.json | 16 ++++ ...ycubic_kobra3v2_ACEPRO_extruder_2.def.json | 16 ++++ ...ycubic_kobra3v2_ACEPRO_extruder_3.def.json | 16 ++++ .../anycubic_kobra3v2_extruder_0.def.json | 16 ++++ .../meshes/anycubic_kobra3v2_buildplate.stl | Bin 0 -> 4284 bytes 8 files changed, 201 insertions(+) create mode 100644 resources/definitions/anycubic_kobra3v2.def.json create mode 100644 resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json create mode 100644 resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_0.def.json create mode 100644 resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_1.def.json create mode 100644 resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_2.def.json create mode 100644 resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_3.def.json create mode 100644 resources/extruders/anycubic_kobra3v2_extruder_0.def.json create mode 100644 resources/meshes/anycubic_kobra3v2_buildplate.stl diff --git a/resources/definitions/anycubic_kobra3v2.def.json b/resources/definitions/anycubic_kobra3v2.def.json new file mode 100644 index 0000000000..59afb1c660 --- /dev/null +++ b/resources/definitions/anycubic_kobra3v2.def.json @@ -0,0 +1,52 @@ +{ + "version": 2, + "name": "Anycubic Kobra 3 v2", + "inherits": "fdmprinter", + "metadata": + { + "visible": true, + "author": "Sam Bonnekamp", + "manufacturer": "Anycubic", + "file_formats": "text/x-gcode", + "platform": "anycubic_kobra3v2_buildplate.STL", + "machine_extruder_trains": { "0": "anycubic_kobra3v2_extruder_0" } + }, + "overrides": + { + "machine_height": { "default_value": 260 }, + "machine_width": { "default_value": 250 }, + "machine_depth": { "default_value": 250 }, + "machine_name": + { + "description": "Anycubic Kobra 3 v2", + "default_value": "Anycubic Kobra 3 v2" + }, + "machine_buildplate_type": { "default_value": "PEI Spring Steel" }, + "machine_heated_bed": { "default_value": true}, + "material_bed_temperature": + { + "default_value": 60, + "maximum_value": "110", + "maximum_value_warning": "90" + }, + "material_print_temp_wait": { "value": true }, + "machine_center_is_zero": { "default_value": false }, + "layer_height": { "default_value": 0.2 }, + "speed_slowdown_layers": { "value": 2 }, + "material_diameter": { "default_value": 1.75 }, + "material_initial_print_temperature": + { + "maximum_value_warning": 295, + "value": "material_print_temperature + 5" + }, + "material_print_temperature": { "maximum_value_warning": 250 }, + "material_print_temperature_layer_0": + { + "maximum_value_warning": 295, + "value": "material_print_temperature + 5" + }, + "material_print_temp_prep": { "default_value": false }, + "machine_start_gcode_first": { "default_value": true }, + "machine_start_gcode": { "default_value": "; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\nTYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nM83 ; use relative distances for extrusion\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" } + } +} diff --git a/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json b/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json new file mode 100644 index 0000000000..ab2f9a3b95 --- /dev/null +++ b/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json @@ -0,0 +1,69 @@ +{ + "version": 2, + "name": "Anycubic Kobra 3 v2 ACE PRO", + "inherits": "fdmprinter", + "metadata": + { + "visible": true, + "author": "Sam Bonnekamp", + "manufacturer": "Anycubic", + "file_formats": "text/x-gcode", + "platform": "anycubic_kobra3v2_buildplate.STL", + "machine_extruder_trains": { + "0": "anycubic_kobra3v2_ACEPRO_extruder_0", + "1": "anycubic_kobra3v2_ACEPRO_extruder_1", + "2": "anycubic_kobra3v2_ACEPRO_extruder_2", + "3": "anycubic_kobra3v2_ACEPRO_extruder_3" + } + }, + "overrides": + { + "machine_height": { "default_value": 260 }, + "machine_width": { "default_value": 250 }, + "machine_depth": { "default_value": 250 }, + "machine_name": + { + "description": "Anycubic Kobra 3 v2", + "default_value": "Anycubic Kobra 3 v2" + }, + "machine_extruder_count": { "default_value": 4 }, + "machine_buildplate_type": { "default_value": "PEI Spring Steel" }, + "machine_heated_bed": { "default_value": true}, + "material_bed_temperature": + { + "default_value": 60, + "maximum_value": "110", + "maximum_value_warning": "90" + }, + "material_print_temp_wait": { "value": true }, + "machine_center_is_zero": { "default_value": false }, + "layer_height": { "default_value": 0.2 }, + "speed_slowdown_layers": { "value": 2 }, + "material_diameter": { "default_value": 1.75 }, + "material_initial_print_temperature": + { + "maximum_value_warning": 295, + "value": "material_print_temperature + 5" + }, + "default_material_print_temperature": + { + "maximum_value": 300, + "default_value": 200 + }, + "material_print_temperature": + { + "maximum_value": 300, + "default_value": 200 + }, + "material_standby_temperature" : {"default_value": "material_print_temperature"}, + "material_print_temperature_layer_0": + { + "maximum_value_warning": 295, + "value": "material_print_temperature + 5" + }, + "material_print_temp_prep": { "default_value": false }, + "machine_start_gcode_first": { "default_value": true }, + "machine_start_gcode": { "default_value": "; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\nTYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nM83 ; use relative distances for extrusion\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" }, + "machine_end_gcode": { "default_value": "M104 S0\nM140 S0\n;Retract the filament\nG92 E1\nG1 E-1 F300\nG27 P2 ;Park toolhead\nM84" } + } +} diff --git a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_0.def.json b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_0.def.json new file mode 100644 index 0000000000..93c8618bf4 --- /dev/null +++ b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_0.def.json @@ -0,0 +1,16 @@ +{ + "version": 2, + "name": "ACE Pro Colour 1", + "inherits": "fdmextruder", + "metadata": + { + "machine": "anycubic_kobra3v2_ACE_PRO", + "position": "0" + }, + "overrides": + { + "extruder_nr": { "default_value": 0 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} diff --git a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_1.def.json b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_1.def.json new file mode 100644 index 0000000000..ed41ca1946 --- /dev/null +++ b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_1.def.json @@ -0,0 +1,16 @@ +{ + "version": 2, + "name": "ACE Pro Colour 2", + "inherits": "fdmextruder", + "metadata": + { + "machine": "anycubic_kobra3v2_ACE_PRO", + "position": "1" + }, + "overrides": + { + "extruder_nr": { "default_value": 1 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} diff --git a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_2.def.json b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_2.def.json new file mode 100644 index 0000000000..2fc2705ebb --- /dev/null +++ b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_2.def.json @@ -0,0 +1,16 @@ +{ + "version": 2, + "name": "ACE Pro Colour 3", + "inherits": "fdmextruder", + "metadata": + { + "machine": "anycubic_kobra3v2_ACE_PRO", + "position": "1" + }, + "overrides": + { + "extruder_nr": { "default_value": 2 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} diff --git a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_3.def.json b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_3.def.json new file mode 100644 index 0000000000..f5ef25efc5 --- /dev/null +++ b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_3.def.json @@ -0,0 +1,16 @@ +{ + "version": 2, + "name": "ACE Pro Colour 4", + "inherits": "fdmextruder", + "metadata": + { + "machine": "anycubic_kobra3v2_ACE_PRO", + "position": "3" + }, + "overrides": + { + "extruder_nr": { "default_value": 3 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} diff --git a/resources/extruders/anycubic_kobra3v2_extruder_0.def.json b/resources/extruders/anycubic_kobra3v2_extruder_0.def.json new file mode 100644 index 0000000000..dba5e6e559 --- /dev/null +++ b/resources/extruders/anycubic_kobra3v2_extruder_0.def.json @@ -0,0 +1,16 @@ +{ + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": + { + "machine": "anycubic_kobra3v2", + "position": "0" + }, + "overrides": + { + "extruder_nr": { "default_value": 0 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} diff --git a/resources/meshes/anycubic_kobra3v2_buildplate.stl b/resources/meshes/anycubic_kobra3v2_buildplate.stl new file mode 100644 index 0000000000000000000000000000000000000000..9d526d0edada625da668ad41379a2d299965b68b GIT binary patch literal 4284 zcmbW4KWmgh5XKihvCvA;I-OXG1nl1P{03q#F|h~+Jx~j+_G{Qks^AAmeRqkCpFu$o zY;24m_$Q6d^Ugju&+gqT;=<+TotfXx&g|^ImnV<+c6TB#zWwbf}<$2iC)MBt!q!EMqVFDHY57x#m7U#p@&J*WudNx-56W$1k{k3-vU z#vqLt)YCumP>T|jL0C`COJCX3%wljqNFxUI!vri!P=?+I^FR)a!FdsbdiqBmYEgo+ z%wJ#-=iihwDDkKlBD<^>#v54G+=+`#49270z3?|T1ayqY7@#qJ*nWJqk3YRd{~UWB zoEHT3FAjzXSd^d)y$`Mm^D_qLMGWfcA9<)n3Ci{ScmlyaC(Rhp`{-AcT_%C2mcVm8 zKr;Gx@oN9WK;41@!J5>><(C&G*DsAq_tXS$nSSjP_SpN6S53yMppStVvCntj-CWK) zOsJ(LuD$+U$H>?dB_PvlI1jZbvGe)La)prvVkl92MGb30tE+_OG)&Z9QNxyCm!R%5 ztIfI|3#Q)>^Wsh^!FSY}9TY;nqTeEdpAoevq1_Wq-Mji$tNU1q+7k0>FD(7Duc~^J z+4*3tf?g6PU{z}2dkOX7`(w3MN~qUgc5uQ|RL{%z$7&3nOTD0HPDsE$Hh2bUQDRmT z4)nju*ws)mUP{rG#Z?Gq3l(eC%SXPd;#$6P7bhyEI3Fp+4z`3^TEfq~5ZN;CbnPyq z3K-1XpZFJqT39RCqYCuKcN;?qtw=B}pq#n1~Lze z5^*cJr@OaA?WGk73NgZ>gr0)%_ZUIcUjNd&Ns^O6kP@H~3lp$I-Q~ zuN7{%^~89gM9Ae%%(kLAmZfXgc%hG5v?t`2(ceBP1q}kfL%)|iB6@vCg?^YAFJdK1 zgZO0gpFQWlceSG0yP+Q@#tS7vF2uaV$huQ8uiotqA-BHg!OjR8gpKSSQs3tquU=Kp O^gX4-c%ekdi17zYNr Date: Wed, 18 Jun 2025 02:39:22 +0800 Subject: [PATCH 043/159] removed redudant fields --- .../definitions/anycubic_kobra3v2.def.json | 18 +++++++++++++----- .../anycubic_kobra3v2_ACE_PRO.def.json | 6 ++---- ...nycubic_kobra3v2_ACEPRO_extruder_2.def.json | 2 +- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/resources/definitions/anycubic_kobra3v2.def.json b/resources/definitions/anycubic_kobra3v2.def.json index 59afb1c660..ec37f3174a 100644 --- a/resources/definitions/anycubic_kobra3v2.def.json +++ b/resources/definitions/anycubic_kobra3v2.def.json @@ -20,15 +20,24 @@ { "description": "Anycubic Kobra 3 v2", "default_value": "Anycubic Kobra 3 v2" - }, + }, "machine_buildplate_type": { "default_value": "PEI Spring Steel" }, "machine_heated_bed": { "default_value": true}, + "material_bed_temperature": - { - "default_value": 60, - "maximum_value": "110", + { "maximum_value": "110", "maximum_value_warning": "90" }, + "default_material_print_temperature": + { + "maximum_value": 300, + "default_value": 210 + }, + "material_print_temperature": + { + "maximum_value": 300, + "default_value": 210 + }, "material_print_temp_wait": { "value": true }, "machine_center_is_zero": { "default_value": false }, "layer_height": { "default_value": 0.2 }, @@ -45,7 +54,6 @@ "maximum_value_warning": 295, "value": "material_print_temperature + 5" }, - "material_print_temp_prep": { "default_value": false }, "machine_start_gcode_first": { "default_value": true }, "machine_start_gcode": { "default_value": "; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\nTYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nM83 ; use relative distances for extrusion\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" } } diff --git a/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json b/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json index ab2f9a3b95..a201ab32f4 100644 --- a/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json +++ b/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json @@ -31,7 +31,6 @@ "machine_heated_bed": { "default_value": true}, "material_bed_temperature": { - "default_value": 60, "maximum_value": "110", "maximum_value_warning": "90" }, @@ -48,12 +47,12 @@ "default_material_print_temperature": { "maximum_value": 300, - "default_value": 200 + "default_value": 210 }, "material_print_temperature": { "maximum_value": 300, - "default_value": 200 + "default_value": 210 }, "material_standby_temperature" : {"default_value": "material_print_temperature"}, "material_print_temperature_layer_0": @@ -61,7 +60,6 @@ "maximum_value_warning": 295, "value": "material_print_temperature + 5" }, - "material_print_temp_prep": { "default_value": false }, "machine_start_gcode_first": { "default_value": true }, "machine_start_gcode": { "default_value": "; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\nTYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nM83 ; use relative distances for extrusion\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" }, "machine_end_gcode": { "default_value": "M104 S0\nM140 S0\n;Retract the filament\nG92 E1\nG1 E-1 F300\nG27 P2 ;Park toolhead\nM84" } diff --git a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_2.def.json b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_2.def.json index 2fc2705ebb..2761e3d08e 100644 --- a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_2.def.json +++ b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_2.def.json @@ -5,7 +5,7 @@ "metadata": { "machine": "anycubic_kobra3v2_ACE_PRO", - "position": "1" + "position": "2" }, "overrides": { From c5550695d664678ab6cafb8f85f4423a147e2b9a Mon Sep 17 00:00:00 2001 From: Sam Bonnekamp Date: Wed, 18 Jun 2025 12:39:29 +0800 Subject: [PATCH 044/159] removed redundant directives, removed colonialism --- resources/definitions/anycubic_kobra3v2.def.json | 15 ++------------- .../anycubic_kobra3v2_ACE_PRO.def.json | 16 +++------------- .../anycubic_kobra3v2_ACEPRO_extruder_0.def.json | 4 ++-- .../anycubic_kobra3v2_ACEPRO_extruder_1.def.json | 4 ++-- .../anycubic_kobra3v2_ACEPRO_extruder_2.def.json | 2 +- .../anycubic_kobra3v2_ACEPRO_extruder_3.def.json | 4 ++-- .../anycubic_kobra3v2_extruder_0.def.json | 2 +- 7 files changed, 13 insertions(+), 34 deletions(-) diff --git a/resources/definitions/anycubic_kobra3v2.def.json b/resources/definitions/anycubic_kobra3v2.def.json index ec37f3174a..8c24b1391c 100644 --- a/resources/definitions/anycubic_kobra3v2.def.json +++ b/resources/definitions/anycubic_kobra3v2.def.json @@ -8,7 +8,7 @@ "author": "Sam Bonnekamp", "manufacturer": "Anycubic", "file_formats": "text/x-gcode", - "platform": "anycubic_kobra3v2_buildplate.STL", + "platform": "anycubic_kobra3v2_buildplate.stl", "machine_extruder_trains": { "0": "anycubic_kobra3v2_extruder_0" } }, "overrides": @@ -27,21 +27,10 @@ "material_bed_temperature": { "maximum_value": "110", "maximum_value_warning": "90" - }, - "default_material_print_temperature": - { - "maximum_value": 300, - "default_value": 210 }, - "material_print_temperature": - { - "maximum_value": 300, - "default_value": 210 - }, - "material_print_temp_wait": { "value": true }, + "material_print_temperature":{ "maximum_value": 300 }, "machine_center_is_zero": { "default_value": false }, "layer_height": { "default_value": 0.2 }, - "speed_slowdown_layers": { "value": 2 }, "material_diameter": { "default_value": 1.75 }, "material_initial_print_temperature": { diff --git a/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json b/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json index a201ab32f4..6b9c65e59a 100644 --- a/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json +++ b/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json @@ -8,7 +8,7 @@ "author": "Sam Bonnekamp", "manufacturer": "Anycubic", "file_formats": "text/x-gcode", - "platform": "anycubic_kobra3v2_buildplate.STL", + "platform": "anycubic_kobra3v2_buildplate.stl", "machine_extruder_trains": { "0": "anycubic_kobra3v2_ACEPRO_extruder_0", "1": "anycubic_kobra3v2_ACEPRO_extruder_1", @@ -37,23 +37,13 @@ "material_print_temp_wait": { "value": true }, "machine_center_is_zero": { "default_value": false }, "layer_height": { "default_value": 0.2 }, - "speed_slowdown_layers": { "value": 2 }, "material_diameter": { "default_value": 1.75 }, "material_initial_print_temperature": { "maximum_value_warning": 295, "value": "material_print_temperature + 5" - }, - "default_material_print_temperature": - { - "maximum_value": 300, - "default_value": 210 - }, - "material_print_temperature": - { - "maximum_value": 300, - "default_value": 210 - }, + }, + "material_print_temperature": { "maximum_value": 300 }, "material_standby_temperature" : {"default_value": "material_print_temperature"}, "material_print_temperature_layer_0": { diff --git a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_0.def.json b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_0.def.json index 93c8618bf4..104d6bb0f1 100644 --- a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_0.def.json +++ b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_0.def.json @@ -1,10 +1,10 @@ { "version": 2, - "name": "ACE Pro Colour 1", + "name": "ACE Pro Color 1", "inherits": "fdmextruder", "metadata": { - "machine": "anycubic_kobra3v2_ACE_PRO", + "machine": "kobra3v2_ACE_PRO", "position": "0" }, "overrides": diff --git a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_1.def.json b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_1.def.json index ed41ca1946..2c3d072215 100644 --- a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_1.def.json +++ b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_1.def.json @@ -1,10 +1,10 @@ { "version": 2, - "name": "ACE Pro Colour 2", + "name": "ACE Pro Color 2", "inherits": "fdmextruder", "metadata": { - "machine": "anycubic_kobra3v2_ACE_PRO", + "machine": "kobra3v2_ACE_PRO", "position": "1" }, "overrides": diff --git a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_2.def.json b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_2.def.json index 2761e3d08e..fb732d5fd4 100644 --- a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_2.def.json +++ b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_2.def.json @@ -1,6 +1,6 @@ { "version": 2, - "name": "ACE Pro Colour 3", + "name": "ACE Pro Color 3", "inherits": "fdmextruder", "metadata": { diff --git a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_3.def.json b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_3.def.json index f5ef25efc5..fb64b590fc 100644 --- a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_3.def.json +++ b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_3.def.json @@ -1,10 +1,10 @@ { "version": 2, - "name": "ACE Pro Colour 4", + "name": "ACE Pro Color 4", "inherits": "fdmextruder", "metadata": { - "machine": "anycubic_kobra3v2_ACE_PRO", + "machine": "kobra3v2_ACE_PRO", "position": "3" }, "overrides": diff --git a/resources/extruders/anycubic_kobra3v2_extruder_0.def.json b/resources/extruders/anycubic_kobra3v2_extruder_0.def.json index dba5e6e559..d96e91ce07 100644 --- a/resources/extruders/anycubic_kobra3v2_extruder_0.def.json +++ b/resources/extruders/anycubic_kobra3v2_extruder_0.def.json @@ -4,7 +4,7 @@ "inherits": "fdmextruder", "metadata": { - "machine": "anycubic_kobra3v2", + "machine": "kobra3v2_ACE_PRO", "position": "0" }, "overrides": From bbe0b7f9f5a30adeb546f7501f5c2aba8e32ccc9 Mon Sep 17 00:00:00 2001 From: Sam Bonnekamp Date: Wed, 18 Jun 2025 13:14:53 +0800 Subject: [PATCH 045/159] missing comment semicolon --- resources/definitions/anycubic_kobra3v2.def.json | 2 +- resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/anycubic_kobra3v2.def.json b/resources/definitions/anycubic_kobra3v2.def.json index 8c24b1391c..8ca9800fde 100644 --- a/resources/definitions/anycubic_kobra3v2.def.json +++ b/resources/definitions/anycubic_kobra3v2.def.json @@ -44,6 +44,6 @@ "value": "material_print_temperature + 5" }, "machine_start_gcode_first": { "default_value": true }, - "machine_start_gcode": { "default_value": "; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\nTYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nM83 ; use relative distances for extrusion\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" } + "machine_start_gcode": { "default_value": "; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\n;TYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nM83 ; use relative distances for extrusion\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" } } } diff --git a/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json b/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json index 6b9c65e59a..9686c4c192 100644 --- a/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json +++ b/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json @@ -51,7 +51,7 @@ "value": "material_print_temperature + 5" }, "machine_start_gcode_first": { "default_value": true }, - "machine_start_gcode": { "default_value": "; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\nTYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nM83 ; use relative distances for extrusion\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" }, + "machine_start_gcode": { "default_value": "; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\n;TYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nM83 ; use relative distances for extrusion\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" }, "machine_end_gcode": { "default_value": "M104 S0\nM140 S0\n;Retract the filament\nG92 E1\nG1 E-1 F300\nG27 P2 ;Park toolhead\nM84" } } } From bfbc6e4dc8269ca51e23e84af3072417f9e9015b Mon Sep 17 00:00:00 2001 From: Sam Bonnekamp Date: Wed, 18 Jun 2025 13:27:02 +0800 Subject: [PATCH 046/159] update printer names --- .../extruders/anycubic_kobra3v2_ACEPRO_extruder_0.def.json | 2 +- .../extruders/anycubic_kobra3v2_ACEPRO_extruder_1.def.json | 2 +- .../extruders/anycubic_kobra3v2_ACEPRO_extruder_3.def.json | 2 +- resources/extruders/anycubic_kobra3v2_extruder_0.def.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_0.def.json b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_0.def.json index 104d6bb0f1..ab4a7d1a68 100644 --- a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_0.def.json +++ b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_0.def.json @@ -4,7 +4,7 @@ "inherits": "fdmextruder", "metadata": { - "machine": "kobra3v2_ACE_PRO", + "machine": "anycubic_kobra3v2_ACE_PRO", "position": "0" }, "overrides": diff --git a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_1.def.json b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_1.def.json index 2c3d072215..029e7ad2bf 100644 --- a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_1.def.json +++ b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_1.def.json @@ -4,7 +4,7 @@ "inherits": "fdmextruder", "metadata": { - "machine": "kobra3v2_ACE_PRO", + "machine": "anycubic_kobra3v2_ACE_PRO", "position": "1" }, "overrides": diff --git a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_3.def.json b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_3.def.json index fb64b590fc..2f64b0532a 100644 --- a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_3.def.json +++ b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_3.def.json @@ -4,7 +4,7 @@ "inherits": "fdmextruder", "metadata": { - "machine": "kobra3v2_ACE_PRO", + "machine": "anycubic_kobra3v2_ACE_PRO", "position": "3" }, "overrides": diff --git a/resources/extruders/anycubic_kobra3v2_extruder_0.def.json b/resources/extruders/anycubic_kobra3v2_extruder_0.def.json index d96e91ce07..dba5e6e559 100644 --- a/resources/extruders/anycubic_kobra3v2_extruder_0.def.json +++ b/resources/extruders/anycubic_kobra3v2_extruder_0.def.json @@ -4,7 +4,7 @@ "inherits": "fdmextruder", "metadata": { - "machine": "kobra3v2_ACE_PRO", + "machine": "anycubic_kobra3v2", "position": "0" }, "overrides": From 4f52a3d9389e192a7be2e32d19f1e5172b1e2580 Mon Sep 17 00:00:00 2001 From: Sam Bonnekamp Date: Wed, 18 Jun 2025 17:21:03 +0800 Subject: [PATCH 047/159] machine uses relative extrusion --- .../definitions/anycubic_kobra3v2.def.json | 21 ++++++++++++------- .../anycubic_kobra3v2_ACE_PRO.def.json | 19 ++++++++++------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/resources/definitions/anycubic_kobra3v2.def.json b/resources/definitions/anycubic_kobra3v2.def.json index 8ca9800fde..a6d1c26d33 100644 --- a/resources/definitions/anycubic_kobra3v2.def.json +++ b/resources/definitions/anycubic_kobra3v2.def.json @@ -8,8 +8,9 @@ "author": "Sam Bonnekamp", "manufacturer": "Anycubic", "file_formats": "text/x-gcode", + "has_textured_buildplate": true, "platform": "anycubic_kobra3v2_buildplate.stl", - "machine_extruder_trains": { "0": "anycubic_kobra3v2_extruder_0" } + "machine_extruder_trains": { "0": "anycubic_kobra3v2_extruder_0" } }, "overrides": { @@ -20,17 +21,19 @@ { "description": "Anycubic Kobra 3 v2", "default_value": "Anycubic Kobra 3 v2" - }, + }, "machine_buildplate_type": { "default_value": "PEI Spring Steel" }, "machine_heated_bed": { "default_value": true}, - + "machine_center_is_zero": { "default_value": false }, + "machine_start_gcode_first": { "default_value": true }, + "machine_start_gcode": { "default_value": "; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\n;TYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" }, + "machine_end_gcode": { "default_value": "M104 S0\nM140 S0\n;Retract the filament\nG92 E1\nG1 E-1 F300\nG27 P2 ;Park toolhead\nM84" } + "material_bed_temperature": { "maximum_value": "110", - "maximum_value_warning": "90" + "maximum_value_warning": "90" }, "material_print_temperature":{ "maximum_value": 300 }, - "machine_center_is_zero": { "default_value": false }, - "layer_height": { "default_value": 0.2 }, "material_diameter": { "default_value": 1.75 }, "material_initial_print_temperature": { @@ -43,7 +46,9 @@ "maximum_value_warning": 295, "value": "material_print_temperature + 5" }, - "machine_start_gcode_first": { "default_value": true }, - "machine_start_gcode": { "default_value": "; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\n;TYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nM83 ; use relative distances for extrusion\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" } + + "layer_height": { "default_value": 0.2 }, + "adhesion_type": { "value": "'skirt'" } + "relative_extrusion" : {"default_value": true} } } diff --git a/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json b/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json index 9686c4c192..bc76df7a99 100644 --- a/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json +++ b/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json @@ -8,6 +8,7 @@ "author": "Sam Bonnekamp", "manufacturer": "Anycubic", "file_formats": "text/x-gcode", + "has_textured_buildplate": true, "platform": "anycubic_kobra3v2_buildplate.stl", "machine_extruder_trains": { "0": "anycubic_kobra3v2_ACEPRO_extruder_0", @@ -36,22 +37,26 @@ }, "material_print_temp_wait": { "value": true }, "machine_center_is_zero": { "default_value": false }, - "layer_height": { "default_value": 0.2 }, + "machine_start_gcode_first": { "default_value": true }, + "machine_start_gcode": { "default_value": "; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\n;TYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" }, + "machine_end_gcode": { "default_value": "M104 S0\nM140 S0\n;Retract the filament\nG92 E1\nG1 E-1 F300\nG27 P2 ;Park toolhead\nM84" } + "material_diameter": { "default_value": 1.75 }, "material_initial_print_temperature": - { + { "maximum_value_warning": 295, "value": "material_print_temperature + 5" - }, - "material_print_temperature": { "maximum_value": 300 }, + }, + "material_print_temperature": { "maximum_value": 300 }, "material_standby_temperature" : {"default_value": "material_print_temperature"}, "material_print_temperature_layer_0": { "maximum_value_warning": 295, "value": "material_print_temperature + 5" }, - "machine_start_gcode_first": { "default_value": true }, - "machine_start_gcode": { "default_value": "; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\n;TYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nM83 ; use relative distances for extrusion\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" }, - "machine_end_gcode": { "default_value": "M104 S0\nM140 S0\n;Retract the filament\nG92 E1\nG1 E-1 F300\nG27 P2 ;Park toolhead\nM84" } + + "layer_height": { "default_value": 0.2 }, + "adhesion_type": { "value": "'skirt'" }, + "relative_extrusion" : {"default_value": true} } } From 0ed4be3e94bf80effae215594558dd1bca35a84e Mon Sep 17 00:00:00 2001 From: Sam Bonnekamp Date: Wed, 18 Jun 2025 17:32:45 +0800 Subject: [PATCH 048/159] machine end gcode matches anycubic --- resources/definitions/anycubic_kobra3v2.def.json | 2 +- resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/anycubic_kobra3v2.def.json b/resources/definitions/anycubic_kobra3v2.def.json index a6d1c26d33..597c3578ed 100644 --- a/resources/definitions/anycubic_kobra3v2.def.json +++ b/resources/definitions/anycubic_kobra3v2.def.json @@ -27,7 +27,7 @@ "machine_center_is_zero": { "default_value": false }, "machine_start_gcode_first": { "default_value": true }, "machine_start_gcode": { "default_value": "; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\n;TYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" }, - "machine_end_gcode": { "default_value": "M104 S0\nM140 S0\n;Retract the filament\nG92 E1\nG1 E-1 F300\nG27 P2 ;Park toolhead\nM84" } + "machine_end_gcode": { "default_value": "G1 X5 Y{machine_depth*0.95} F{speed_travel*60} ; present print\nM140 S0 ; turn off heat bed\nM104 S0 ; turn off temperature\nM84; disable motors ; disable stepper motors" }, "material_bed_temperature": { "maximum_value": "110", diff --git a/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json b/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json index bc76df7a99..c1738ddacd 100644 --- a/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json +++ b/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json @@ -39,7 +39,7 @@ "machine_center_is_zero": { "default_value": false }, "machine_start_gcode_first": { "default_value": true }, "machine_start_gcode": { "default_value": "; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\n;TYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" }, - "machine_end_gcode": { "default_value": "M104 S0\nM140 S0\n;Retract the filament\nG92 E1\nG1 E-1 F300\nG27 P2 ;Park toolhead\nM84" } + "machine_end_gcode": { "default_value": "G1 X5 Y{machine_depth*0.95} F{speed_travel*60} ; present print\nM140 S0 ; turn off heat bed\nM104 S0 ; turn off temperature\nM84; disable motors ; disable stepper motors" }, "material_diameter": { "default_value": 1.75 }, "material_initial_print_temperature": From 350c95a110ee4074be8b80b9fe029bc58a7eb865 Mon Sep 17 00:00:00 2001 From: Sam Bonnekamp Date: Wed, 18 Jun 2025 17:36:32 +0800 Subject: [PATCH 049/159] missing semicolon --- resources/definitions/anycubic_kobra3v2.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/anycubic_kobra3v2.def.json b/resources/definitions/anycubic_kobra3v2.def.json index 597c3578ed..0f87ea130d 100644 --- a/resources/definitions/anycubic_kobra3v2.def.json +++ b/resources/definitions/anycubic_kobra3v2.def.json @@ -48,7 +48,7 @@ }, "layer_height": { "default_value": 0.2 }, - "adhesion_type": { "value": "'skirt'" } + "adhesion_type": { "value": "'skirt'" }, "relative_extrusion" : {"default_value": true} } } From 75946d8871b43a1e091ac36900d8c2cbfdd0ffd3 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 18 Jun 2025 11:56:08 +0200 Subject: [PATCH 050/159] Shape of brush is an enum now, not a string. part of CURA-12543 --- plugins/PaintTool/PaintTool.py | 14 ++++++++++---- plugins/PaintTool/PaintTool.qml | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index cbb0c97739..942ca67f17 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -1,6 +1,7 @@ # Copyright (c) 2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. +from enum import IntEnum import numpy from PyQt6.QtCore import Qt from PyQt6.QtGui import QImage, QPainter, QColor, QBrush, QPen @@ -20,6 +21,10 @@ from .PaintView import PaintView class PaintTool(Tool): """Provides the tool to paint meshes.""" + class BrushShape(IntEnum): + SQUARE = 0 + CIRCLE = 1 + def __init__(self) -> None: super().__init__() @@ -31,6 +36,7 @@ class PaintTool(Tool): self._mesh_transformed_cache = None self._cache_dirty: bool = True + # TODO: Colors will need to be replaced on a 'per type of painting' basis. self._color_str_to_rgba: Dict[str, List[int]] = { "A": [192, 0, 192, 255], "B": [232, 128, 0, 255], @@ -40,7 +46,7 @@ class PaintTool(Tool): self._brush_size: int = 10 self._brush_color: str = "A" - self._brush_shape: str = "A" + self._brush_shape: PaintTool.BrushShape = PaintTool.BrushShape.SQUARE self._brush_pen: QPen = self._createBrushPen() self._mouse_held: bool = False @@ -56,9 +62,9 @@ class PaintTool(Tool): color = self._color_str_to_rgba[self._brush_color] pen.setColor(QColor(color[0], color[1], color[2], color[3])) match self._brush_shape: - case "A": + case PaintTool.BrushShape.SQUARE: pen.setCapStyle(Qt.PenCapStyle.SquareCap) - case "B": + case PaintTool.BrushShape.CIRCLE: pen.setCapStyle(Qt.PenCapStyle.RoundCap) return pen @@ -98,7 +104,7 @@ class PaintTool(Tool): self._brush_color = brush_color self._brush_pen = self._createBrushPen() - def setBrushShape(self, brush_shape: str) -> None: + def setBrushShape(self, brush_shape: int) -> None: if brush_shape != self._brush_shape: self._brush_shape = brush_shape self._brush_pen = self._createBrushPen() diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml index a1fac9c3a3..31aeb409d1 100644 --- a/plugins/PaintTool/PaintTool.qml +++ b/plugins/PaintTool/PaintTool.qml @@ -139,7 +139,7 @@ Item z: 2 - onClicked: UM.Controller.triggerActionWithData("setBrushShape", "A") + onClicked: UM.Controller.triggerActionWithData("setBrushShape", 0) } UM.ToolbarButton @@ -156,7 +156,7 @@ Item z: 2 - onClicked: UM.Controller.triggerActionWithData("setBrushShape", "B") + onClicked: UM.Controller.triggerActionWithData("setBrushShape", 1) } UM.Slider From 4fae9b231ab205b36c2297cb63d3801b097c4202 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 18 Jun 2025 12:06:04 +0200 Subject: [PATCH 051/159] Paint: Make calculation of Baricentric-coordinates a bit more robust. part of CURA-12543 --- plugins/PaintTool/PaintTool.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index 942ca67f17..86cc72e2b9 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -137,16 +137,25 @@ class PaintTool(Tool): # find unit direction vector for line C, which is perpendicular to lines A and B udir_res = numpy.cross(udir_b, udir_a) - udir_res /= numpy.linalg.norm(udir_res) + udir_res_len = numpy.linalg.norm(udir_res) + if udir_res_len == 0: + return 1.0 + udir_res /= udir_res_len # solve system of equations rhs = b - a lhs = numpy.array([udir_a, -udir_b, udir_res]).T - solved = numpy.linalg.solve(lhs, rhs) + try: + solved = numpy.linalg.solve(lhs, rhs) + except numpy.linalg.LinAlgError: + return 1.0 # get the ratio intersect = ((a + solved[0] * udir_a) + (b + solved[1] * udir_b)) * 0.5 - return numpy.linalg.norm(pt - intersect) / numpy.linalg.norm(a - intersect) + a_intersect_dist = numpy.linalg.norm(a - intersect) + if a_intersect_dist == 0: + return 1.0 + return numpy.linalg.norm(pt - intersect) / a_intersect_dist def _nodeTransformChanged(self, *args) -> None: self._cache_dirty = True @@ -167,6 +176,8 @@ class PaintTool(Tool): wb = PaintTool._get_intersect_ratio_via_pt(vb, pt, vc, va) wc = PaintTool._get_intersect_ratio_via_pt(vc, pt, va, vb) wt = wa + wb + wc + if wt == 0: + return face_id, None wa /= wt wb /= wt wc /= wt From 01c02e4479d4ca3fd1faf568ffc67bc63a2fa195 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 18 Jun 2025 12:42:45 +0200 Subject: [PATCH 052/159] Paint: Simplify and clarify input-event-code. part of CURA-12543 --- plugins/PaintTool/PaintTool.py | 38 ++++++++++------------------------ plugins/SolidView/SolidView.py | 1 - 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index 86cc72e2b9..d596ea7289 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -4,7 +4,8 @@ from enum import IntEnum import numpy from PyQt6.QtCore import Qt -from PyQt6.QtGui import QImage, QPainter, QColor, QBrush, QPen +from PyQt6.QtGui import QImage, QPainter, QColor, QPen +from PyQt6 import QtWidgets from typing import cast, Dict, List, Optional, Tuple from UM.Application import Application @@ -50,8 +51,6 @@ class PaintTool(Tool): self._brush_pen: QPen = self._createBrushPen() self._mouse_held: bool = False - self._ctrl_held: bool = False - self._shift_held: bool = False self._last_text_coords: Optional[numpy.ndarray] = None self._last_face_id: Optional[int] = None @@ -209,27 +208,14 @@ class PaintTool(Tool): controller.setActiveView("SolidView") return True - if event.type == Event.KeyPressEvent: - evt = cast(KeyEvent, event) - if evt.key == KeyEvent.ControlKey: - self._ctrl_held = True - return True - if evt.key == KeyEvent.ShiftKey: - self._shift_held = True - return True - return False - if event.type == Event.KeyReleaseEvent: - evt = cast(KeyEvent, event) - if evt.key == KeyEvent.ControlKey: - self._ctrl_held = False - return True - if evt.key == KeyEvent.ShiftKey: - self._shift_held = False - return True - if evt.key == Qt.Key.Key_L and self._ctrl_held: + key_release = cast(KeyEvent, event) + modifiers = QtWidgets.QApplication.keyboardModifiers() + ctrl_held = modifiers & Qt.KeyboardModifier.ControlModifier != Qt.KeyboardModifier.NoModifier + if key_release.key == Qt.Key.Key_L and ctrl_held: # NOTE: Ctrl-L is used here instead of Ctrl-Z, as the latter is the application-wide one that takes precedence. - return self.undoStackAction(self._shift_held) + shift_held = modifiers & Qt.KeyboardModifier.ShiftModifier != Qt.KeyboardModifier.NoModifier + return self.undoStackAction(shift_held) return False if event.type == Event.MouseReleaseEvent and self._controller.getToolsEnabled(): @@ -246,9 +232,9 @@ class PaintTool(Tool): if is_moved and not self._mouse_held: return False - evt = cast(MouseEvent, event) + mouse_evt = cast(MouseEvent, event) if is_pressed: - if MouseEvent.LeftButton not in evt.buttons: + if MouseEvent.LeftButton not in mouse_evt.buttons: return False else: self._mouse_held = True @@ -276,15 +262,13 @@ class PaintTool(Tool): if not self._mesh_transformed_cache: return False - evt = cast(MouseEvent, event) - 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, evt.x, evt.y) + face_id, texcoords = self._getTexCoordsFromClick(node, mouse_evt.x, mouse_evt.y) if texcoords is None: return False if self._last_text_coords is None: diff --git a/plugins/SolidView/SolidView.py b/plugins/SolidView/SolidView.py index e115267720..bffc3aa526 100644 --- a/plugins/SolidView/SolidView.py +++ b/plugins/SolidView/SolidView.py @@ -36,7 +36,6 @@ class SolidView(View): _show_xray_warning_preference = "view/show_xray_warning" _show_overhang_preference = "view/show_overhang" - _paint_active_preference = "view/paint_active" def __init__(self): super().__init__() From 56f669d1fd4440831481b238c28c38055427c468 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 18 Jun 2025 12:50:21 +0200 Subject: [PATCH 053/159] Paint: Replace undo-redo UI code with qml Action. part of CURA-12543 --- plugins/PaintTool/PaintTool.py | 10 ---------- plugins/PaintTool/PaintTool.qml | 19 +++++++++++++++++-- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index d596ea7289..e4dab412e7 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -208,16 +208,6 @@ class PaintTool(Tool): controller.setActiveView("SolidView") return True - if event.type == Event.KeyReleaseEvent: - key_release = cast(KeyEvent, event) - modifiers = QtWidgets.QApplication.keyboardModifiers() - ctrl_held = modifiers & Qt.KeyboardModifier.ControlModifier != Qt.KeyboardModifier.NoModifier - if key_release.key == Qt.Key.Key_L and ctrl_held: - # NOTE: Ctrl-L is used here instead of Ctrl-Z, as the latter is the application-wide one that takes precedence. - shift_held = modifiers & Qt.KeyboardModifier.ShiftModifier != Qt.KeyboardModifier.NoModifier - return self.undoStackAction(shift_held) - return False - if event.type == Event.MouseReleaseEvent and self._controller.getToolsEnabled(): if MouseEvent.LeftButton not in cast(MouseEvent, event).buttons: return False diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml index 31aeb409d1..8bd02e00a3 100644 --- a/plugins/PaintTool/PaintTool.qml +++ b/plugins/PaintTool/PaintTool.qml @@ -2,6 +2,7 @@ // Cura is released under the terms of the LGPLv3 or higher. import QtQuick +import QtQuick.Controls import QtQuick.Layouts import UM 1.7 as UM @@ -13,6 +14,20 @@ Item height: childrenRect.height UM.I18nCatalog { id: catalog; name: "cura"} + Action + { + id: undoAction + shortcut: "Ctrl+L" + onTriggered: UM.Controller.triggerActionWithData("undoStackAction", false) + } + + Action + { + id: redoAction + shortcut: "Ctrl+Shift+L" + onTriggered: UM.Controller.triggerActionWithData("undoStackAction", true) + } + ColumnLayout { RowLayout @@ -192,7 +207,7 @@ Item z: 2 - onClicked: UM.Controller.triggerActionWithData("undoStackAction", false) + onClicked: undoAction.trigger() } UM.ToolbarButton @@ -208,7 +223,7 @@ Item z: 2 - onClicked: UM.Controller.triggerActionWithData("undoStackAction", true) + onClicked: redoAction.trigger() } } } From f7dc928d3283878e80ad8694ebc6d24a78cc9fea Mon Sep 17 00:00:00 2001 From: Sam Bonnekamp Date: Wed, 18 Jun 2025 19:03:55 +0800 Subject: [PATCH 054/159] fixed unecessary default --- resources/definitions/anycubic_kobra3v2.def.json | 2 +- resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/anycubic_kobra3v2.def.json b/resources/definitions/anycubic_kobra3v2.def.json index 0f87ea130d..0f32de2604 100644 --- a/resources/definitions/anycubic_kobra3v2.def.json +++ b/resources/definitions/anycubic_kobra3v2.def.json @@ -49,6 +49,6 @@ "layer_height": { "default_value": 0.2 }, "adhesion_type": { "value": "'skirt'" }, - "relative_extrusion" : {"default_value": true} + "relative_extrusion" : {"value": true} } } diff --git a/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json b/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json index c1738ddacd..b85fb3c0ed 100644 --- a/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json +++ b/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json @@ -57,6 +57,6 @@ "layer_height": { "default_value": 0.2 }, "adhesion_type": { "value": "'skirt'" }, - "relative_extrusion" : {"default_value": true} + "relative_extrusion" : {"value": true} } } From d2ade67cad0a670ea82fd0cd4ada9fdc97c7f18b Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 18 Jun 2025 13:41:20 +0200 Subject: [PATCH 055/159] Paint: 'Substrokes per face' data-structure. part of CURA-12543 --- plugins/PaintTool/PaintTool.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index e4dab412e7..db09bae410 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -53,6 +53,7 @@ class PaintTool(Tool): self._mouse_held: bool = False self._last_text_coords: Optional[numpy.ndarray] = None + self._last_mouse_coords: Optional[Tuple[int, int]] = None self._last_face_id: Optional[int] = None def _createBrushPen(self) -> QPen: @@ -213,6 +214,7 @@ class PaintTool(Tool): return False self._mouse_held = False self._last_text_coords = None + self._last_mouse_coords = None self._last_face_id = None return True @@ -263,26 +265,32 @@ class PaintTool(Tool): return False if self._last_text_coords is None: self._last_text_coords = texcoords + self._last_mouse_coords = (mouse_evt.x, mouse_evt.y) self._last_face_id = face_id - if face_id != self._last_face_id: - # TODO: draw two strokes in this case, for the two faces involved - # ... it's worse, for smaller faces we may genuinely require the patch -- and it may even go over _multiple_ patches if the user paints fast enough - # -> for now; make a lookup table for which faces are connected to which, don't split if they are connected, and solve the connection issue(s) later + substrokes_per_face = {} + if face_id == self._last_face_id: + substrokes_per_face[face_id] = (self._last_text_coords, texcoords) + else: + # TODO: In case the stroke doesn't begin and end within the same face: + # Iteratively get the face-id's and texture coordinates of mid-point between the previous-mouse position and this one, ... self._last_text_coords = texcoords + self._last_mouse_coords = (mouse_evt.x, mouse_evt.y) self._last_face_id = face_id return True w, h = paintview.getUvTexDimensions() - sub_image, (start_x, start_y) = self._createStrokeImage( - self._last_text_coords[0] * w, - self._last_text_coords[1] * h, - texcoords[0] * w, - texcoords[1] * h - ) - paintview.addStroke(sub_image, start_x, start_y) + for faceid, (start_coords, end_coords) in substrokes_per_face.items(): + sub_image, (start_x, start_y) = self._createStrokeImage( + start_coords[0] * w, + start_coords[1] * h, + end_coords[0] * w, + end_coords[1] * h + ) + paintview.addStroke(sub_image, start_x, start_y) self._last_text_coords = texcoords + self._last_mouse_coords = (mouse_evt.x, mouse_evt.y) self._last_face_id = face_id Application.getInstance().getController().getScene().sceneChanged.emit(node) return True From e27926a9685c0491902c5efd41b236b61bf18398 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 18 Jun 2025 15:02:06 +0200 Subject: [PATCH 056/159] Paint: Have a stroke properly propagate over the texture-triangles. part of CURA-12543 --- plugins/PaintTool/PaintTool.py | 47 ++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index db09bae410..bd3480161b 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -8,6 +8,8 @@ from PyQt6.QtGui import QImage, QPainter, QColor, QPen from PyQt6 import QtWidgets from typing import cast, Dict, List, Optional, Tuple +from numpy import ndarray + from UM.Application import Application from UM.Event import Event, MouseEvent, KeyEvent from UM.Logger import Logger @@ -160,7 +162,7 @@ class PaintTool(Tool): def _nodeTransformChanged(self, *args) -> None: self._cache_dirty = True - def _getTexCoordsFromClick(self, node: SceneNode, x: int, y: int) -> Tuple[int, Optional[numpy.ndarray]]: + def _getTexCoordsFromClick(self, node: SceneNode, x: float, y: float) -> Tuple[int, Optional[numpy.ndarray]]: face_id = self._selection_pass.getFaceIdAtPosition(x, y) if face_id < 0 or face_id >= node.getMeshData().getFaceCount(): return face_id, None @@ -184,6 +186,34 @@ class PaintTool(Tool): texcoords = wa * ta + wb * tb + wc * tc return face_id, texcoords + def _iteratateSplitSubstroke(self, node, substrokes, + info_a: Tuple[Tuple[float, float], Tuple[int, Optional[numpy.ndarray]]], + info_b: Tuple[Tuple[float, float], Tuple[int, Optional[numpy.ndarray]]]) -> None: + click_a, (face_a, texcoords_a) = info_a + click_b, (face_b, texcoords_b) = info_b + + if (abs(click_a[0] - click_b[0]) < 0.0001 and abs(click_a[1] - click_b[1]) < 0.0001) or (face_a < 0 and face_b < 0): + return + if face_b < 0 or face_a == face_b: + substrokes.append((self._last_text_coords, texcoords_a)) + return + if face_a < 0: + substrokes.append((self._last_text_coords, texcoords_b)) + return + + mouse_mid = (click_a[0] + click_b[0]) / 2.0, (click_a[1] + click_b[1]) / 2.0 + face_mid, texcoords_mid = self._getTexCoordsFromClick(node, mouse_mid[0], mouse_mid[1]) + mid_struct = (mouse_mid, (face_mid, texcoords_mid)) + if face_mid == face_a: + substrokes.append((texcoords_a, texcoords_mid)) + self._iteratateSplitSubstroke(node, substrokes, mid_struct, info_b) + elif face_mid == face_b: + substrokes.append((texcoords_mid, texcoords_b)) + self._iteratateSplitSubstroke(node, substrokes, info_a, mid_struct) + else: + self._iteratateSplitSubstroke(node, substrokes, mid_struct, info_b) + self._iteratateSplitSubstroke(node, substrokes, info_a, mid_struct) + def event(self, event: Event) -> bool: """Handle mouse and keyboard events. @@ -268,19 +298,16 @@ class PaintTool(Tool): self._last_mouse_coords = (mouse_evt.x, mouse_evt.y) self._last_face_id = face_id - substrokes_per_face = {} + substrokes = [] if face_id == self._last_face_id: - substrokes_per_face[face_id] = (self._last_text_coords, texcoords) + substrokes.append((self._last_text_coords, texcoords)) else: - # TODO: In case the stroke doesn't begin and end within the same face: - # Iteratively get the face-id's and texture coordinates of mid-point between the previous-mouse position and this one, ... - self._last_text_coords = texcoords - self._last_mouse_coords = (mouse_evt.x, mouse_evt.y) - self._last_face_id = face_id - return True + self._iteratateSplitSubstroke(node, substrokes, + (self._last_mouse_coords, (self._last_face_id, self._last_text_coords)), + ((mouse_evt.x, mouse_evt.y), (face_id, texcoords))) w, h = paintview.getUvTexDimensions() - for faceid, (start_coords, end_coords) in substrokes_per_face.items(): + for start_coords, end_coords in substrokes: sub_image, (start_x, start_y) = self._createStrokeImage( start_coords[0] * w, start_coords[1] * h, From 4caba52f0533611792cebd8253baf3b76230323f Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 18 Jun 2025 16:00:47 +0200 Subject: [PATCH 057/159] Basically working multipurpose painting CURA-12566 --- cura/Scene/SliceableObjectDecorator.py | 15 ++++- plugins/PaintTool/PaintTool.py | 62 +++++++++++-------- plugins/PaintTool/PaintView.py | 86 +++++++++++++++++++++++++- plugins/PaintTool/paint.shader | 31 ++++++---- resources/themes/cura-light/theme.json | 6 +- 5 files changed, 157 insertions(+), 43 deletions(-) diff --git a/cura/Scene/SliceableObjectDecorator.py b/cura/Scene/SliceableObjectDecorator.py index c26848ed1a..dee244b81c 100644 --- a/cura/Scene/SliceableObjectDecorator.py +++ b/cura/Scene/SliceableObjectDecorator.py @@ -1,6 +1,8 @@ import copy -from typing import Optional +from typing import Optional, Dict + +from PyQt6.QtGui import QImage import UM.View.GL.Texture from UM.Scene.SceneNodeDecorator import SceneNodeDecorator @@ -16,6 +18,7 @@ class SliceableObjectDecorator(SceneNodeDecorator): def __init__(self) -> None: super().__init__() self._paint_texture = None + self._texture_data_mapping: Dict[str, tuple[int, int]] = {} def isSliceable(self) -> bool: return True @@ -23,12 +26,22 @@ class SliceableObjectDecorator(SceneNodeDecorator): def getPaintTexture(self, create_if_required: bool = True) -> Optional[UM.View.GL.Texture.Texture]: if self._paint_texture is None and create_if_required: self._paint_texture = OpenGL.getInstance().createTexture(TEXTURE_WIDTH, TEXTURE_HEIGHT) + image = QImage(TEXTURE_WIDTH, TEXTURE_HEIGHT, QImage.Format.Format_RGB32) + image.fill(0) + self._paint_texture.setImage(image) return self._paint_texture def setPaintTexture(self, texture: UM.View.GL.Texture) -> None: self._paint_texture = texture + def getTextureDataMapping(self) -> Dict[str, tuple[int, int]]: + return self._texture_data_mapping + + def setTextureDataMapping(self, mapping: Dict[str, tuple[int, int]]) -> None: + self._texture_data_mapping = mapping + def __deepcopy__(self, memo) -> "SliceableObjectDecorator": copied_decorator = SliceableObjectDecorator() copied_decorator.setPaintTexture(copy.deepcopy(self.getPaintTexture())) + copied_decorator.setTextureDataMapping(copy.deepcopy(self.getTextureDataMapping())) return copied_decorator diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index cbb0c97739..deddff1a40 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -31,13 +31,6 @@ class PaintTool(Tool): self._mesh_transformed_cache = None self._cache_dirty: bool = True - self._color_str_to_rgba: Dict[str, List[int]] = { - "A": [192, 0, 192, 255], - "B": [232, 128, 0, 255], - "C": [0, 255, 0, 255], - "D": [255, 255, 255, 255], - } - self._brush_size: int = 10 self._brush_color: str = "A" self._brush_shape: str = "A" @@ -53,8 +46,8 @@ class PaintTool(Tool): def _createBrushPen(self) -> QPen: pen = QPen() pen.setWidth(self._brush_size) - color = self._color_str_to_rgba[self._brush_color] - pen.setColor(QColor(color[0], color[1], color[2], color[3])) + pen.setColor(Qt.GlobalColor.white) + match self._brush_shape: case "A": pen.setCapStyle(Qt.PenCapStyle.SquareCap) @@ -70,8 +63,8 @@ class PaintTool(Tool): start_x = int(min(x0, x1) - half_brush_size) start_y = int(min(y0, y1) - half_brush_size) - stroke_image = QImage(abs(xdiff) + self._brush_size, abs(ydiff) + self._brush_size, QImage.Format.Format_RGBA8888) - stroke_image.fill(QColor(0,0,0,0)) + stroke_image = QImage(abs(xdiff) + self._brush_size, abs(ydiff) + self._brush_size, QImage.Format.Format_RGB32) + stroke_image.fill(0) painter = QPainter(stroke_image) painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) @@ -85,8 +78,14 @@ class PaintTool(Tool): return stroke_image, (start_x, start_y) def setPaintType(self, paint_type: str) -> None: - Logger.warning(f"TODO: Implement paint-types ({paint_type}).") - pass # FIXME: ... and also please call `self._brush_pen = self._createBrushPen()` (see other funcs). + paint_view = self._get_paint_view() + if paint_view is None: + return + + paint_view.setPaintType(paint_type) + + self._brush_pen = self._createBrushPen() + self._updateScene() def setBrushSize(self, brush_size: float) -> None: if brush_size != self._brush_size: @@ -94,9 +93,7 @@ class PaintTool(Tool): self._brush_pen = self._createBrushPen() def setBrushColor(self, brush_color: str) -> None: - if brush_color != self._brush_color: - self._brush_color = brush_color - self._brush_pen = self._createBrushPen() + self._brush_color = brush_color def setBrushShape(self, brush_shape: str) -> None: if brush_shape != self._brush_shape: @@ -104,19 +101,25 @@ class PaintTool(Tool): self._brush_pen = self._createBrushPen() def undoStackAction(self, redo_instead: bool) -> bool: - paintview = Application.getInstance().getController().getActiveView() - if paintview is None or paintview.getPluginId() != "PaintTool": + paint_view = self._get_paint_view() + if paint_view is None: return False - paintview = cast(PaintView, paintview) + if redo_instead: - paintview.redoStroke() + paint_view.redoStroke() else: - paintview.undoStroke() - node = Selection.getSelectedObject(0) - if node is not None: - Application.getInstance().getController().getScene().sceneChanged.emit(node) + paint_view.undoStroke() + + self._updateScene() return True + @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 @@ -289,11 +292,18 @@ class PaintTool(Tool): texcoords[0] * w, texcoords[1] * h ) - paintview.addStroke(sub_image, start_x, start_y) + paintview.addStroke(sub_image, start_x, start_y, self._brush_color) self._last_text_coords = texcoords self._last_face_id = face_id - Application.getInstance().getController().getScene().sceneChanged.emit(node) + self._updateScene(node) return True return False + + @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) \ No newline at end of file diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index 83f554dbed..71bf6cb014 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -2,10 +2,11 @@ # Cura is released under the terms of the LGPLv3 or higher. import os -from typing import Optional, List, Tuple +from typing import Optional, List, Tuple, Dict from PyQt6.QtGui import QImage, QColor, QPainter +from cura.CuraApplication import CuraApplication from UM.PluginRegistry import PluginRegistry from UM.View.GL.ShaderProgram import ShaderProgram from UM.View.GL.Texture import Texture @@ -13,6 +14,7 @@ from UM.View.View import View from UM.Scene.Selection import Selection from UM.View.GL.OpenGL import OpenGL from UM.i18n import i18nCatalog +from UM.Math.Color import Color catalog = i18nCatalog("cura") @@ -22,10 +24,24 @@ class PaintView(View): UNDO_STACK_SIZE = 1024 + class PaintType: + def __init__(self, icon: str, display_color: Color, value: int): + self.icon: str = icon + self.display_color: Color = display_color + self.value: int = value + + class PaintMode: + def __init__(self, icon: str, types: Dict[str, "PaintView.PaintType"]): + self.icon: str = icon + self.types = types + def __init__(self) -> None: super().__init__() 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, PaintView.PaintMode] = {} self._stroke_undo_stack: List[Tuple[QImage, int, int]] = [] self._stroke_redo_stack: List[Tuple[QImage, int, int]] = [] @@ -33,6 +49,18 @@ class PaintView(View): self._force_opaque_mask = QImage(2, 2, QImage.Format.Format_Mono) self._force_opaque_mask.fill(1) + CuraApplication.getInstance().engineCreatedSignal.connect(self._makePaintModes) + + def _makePaintModes(self): + theme = CuraApplication.getInstance().getTheme() + usual_types = {"A": self.PaintType("Buildplate", Color(*theme.getColor("paint_normal_area").getRgb()), 0), + "B": self.PaintType("BlackMagic", Color(*theme.getColor("paint_preferred_area").getRgb()), 1), + "C": self.PaintType("Eye", Color(*theme.getColor("paint_avoid_area").getRgb()), 2)} + self._paint_modes = { + "A": self.PaintMode("MeshTypeNormal", usual_types), + "B": self.PaintMode("CircleOutline", usual_types), + } + def _checkSetup(self): if not self._paint_shader: shader_filename = os.path.join(PluginRegistry.getInstance().getPluginPath("PaintTool"), "paint.shader") @@ -49,10 +77,26 @@ class PaintView(View): res.setAlphaChannel(self._force_opaque_mask.scaled(image.width(), image.height())) return res - def addStroke(self, stroke_image: QImage, start_x: int, start_y: int) -> None: - if self._current_paint_texture is None: + def addStroke(self, stroke_image: QImage, start_x: int, start_y: int, brush_color: str) -> None: + if self._current_paint_texture is None or self._current_paint_texture.getImage() is None: return + actual_image = self._current_paint_texture.getImage() + + bit_range_start, bit_range_end = self._current_bits_ranges + set_value = self._paint_modes[self._current_paint_type].types[brush_color].value << self._current_bits_ranges[0] + clear_mask = 0xffffffff ^ (((0xffffffff << (32 - 1 - (bit_range_end - bit_range_start))) & 0xffffffff) >> (32 - 1 - bit_range_end)) + + for x in range(stroke_image.width()): + for y in range(stroke_image.height()): + stroke_pixel = stroke_image.pixel(x, y) + actual_pixel = actual_image.pixel(start_x + x, start_y + y) + if stroke_pixel != 0: + new_pixel = (actual_pixel & clear_mask) | set_value + else: + new_pixel = actual_pixel + stroke_image.setPixel(x, y, new_pixel) + self._stroke_redo_stack.clear() if len(self._stroke_undo_stack) >= PaintView.UNDO_STACK_SIZE: self._stroke_undo_stack.pop(0) @@ -83,6 +127,31 @@ class PaintView(View): return self._current_paint_texture.getWidth(), self._current_paint_texture.getHeight() return 0, 0 + def setPaintType(self, paint_type: str) -> None: + 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].types)) + paint_data_mapping[paint_type] = new_mapping + node.callDecoration("setTextureDataMapping", paint_data_mapping) + + self._current_paint_type = paint_type + self._current_bits_ranges = paint_data_mapping[paint_type] + + @staticmethod + def _add_mapping(actual_mapping: Dict[str, tuple[int, int]], nb_storable_values: int) -> tuple[int, int]: + start_index = 0 + if actual_mapping: + start_index = max(end_index for _, end_index in actual_mapping.values()) + 1 + + end_index = start_index + int.bit_length(nb_storable_values - 1) - 1 + + return start_index, end_index + def beginRendering(self) -> None: renderer = self.getRenderer() self._checkSetup() @@ -93,6 +162,17 @@ class PaintView(View): if node is None: return + if self._current_paint_type == "": + self.setPaintType("A") + + self._paint_shader.setUniformValue("u_bitsRangesStart", self._current_bits_ranges[0]) + self._paint_shader.setUniformValue("u_bitsRangesEnd", self._current_bits_ranges[1]) + + colors = [paint_type_obj.display_color for paint_type_obj in self._paint_modes[self._current_paint_type].types.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()) diff --git a/plugins/PaintTool/paint.shader b/plugins/PaintTool/paint.shader index 83682c7222..bd769f5cb2 100644 --- a/plugins/PaintTool/paint.shader +++ b/plugins/PaintTool/paint.shader @@ -27,11 +27,13 @@ vertex = fragment = uniform mediump vec4 u_ambientColor; - uniform mediump vec4 u_diffuseColor; 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; + uniform mediump vec3 u_renderColors[16]; varying highp vec3 v_vertex; varying highp vec3 v_normal; @@ -48,15 +50,17 @@ fragment = highp vec3 light_dir = normalize(u_lightPosition - v_vertex); /* Diffuse Component */ + ivec4 texture = ivec4(texture(u_texture, v_uvs) * 255.0); + uint color_index = (texture.r << 16) | (texture.g << 8) | texture.b; + color_index = (color_index << (32 - 1 - u_bitsRangesEnd)) >> 32 - 1 - (u_bitsRangesEnd - u_bitsRangesStart); + + vec4 diffuse_color = vec4(u_renderColors[color_index] / 255.0, 1.0); highp float n_dot_l = clamp(dot(normal, light_dir), 0.0, 1.0); - final_color += (n_dot_l * u_diffuseColor); + final_color += (n_dot_l * diffuse_color); final_color.a = u_opacity; - lowp vec4 texture = texture2D(u_texture, v_uvs); - final_color = mix(final_color, texture, texture.a); - - gl_FragColor = final_color; + frag_color = final_color; } vertex41core = @@ -89,11 +93,13 @@ vertex41core = fragment41core = #version 410 uniform mediump vec4 u_ambientColor; - uniform mediump vec4 u_diffuseColor; 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; + uniform mediump vec3 u_renderColors[16]; in highp vec3 v_vertex; in highp vec3 v_normal; @@ -111,20 +117,21 @@ fragment41core = highp vec3 light_dir = normalize(u_lightPosition - v_vertex); /* Diffuse Component */ + ivec4 texture = ivec4(texture(u_texture, v_uvs) * 255.0); + uint color_index = (texture.r << 16) | (texture.g << 8) | texture.b; + color_index = (color_index << (32 - 1 - u_bitsRangesEnd)) >> 32 - 1 - (u_bitsRangesEnd - u_bitsRangesStart); + + vec4 diffuse_color = vec4(u_renderColors[color_index] / 255.0, 1.0); highp float n_dot_l = clamp(dot(normal, light_dir), 0.0, 1.0); - final_color += (n_dot_l * u_diffuseColor); + final_color += (n_dot_l * diffuse_color); final_color.a = u_opacity; - lowp vec4 texture = texture(u_texture, v_uvs); - final_color = mix(final_color, texture, texture.a); - frag_color = final_color; } [defaults] u_ambientColor = [0.3, 0.3, 0.3, 1.0] -u_diffuseColor = [1.0, 1.0, 1.0, 1.0] u_opacity = 0.5 u_texture = 0 diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 1ae316f96c..c5684a416b 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -501,7 +501,11 @@ "error_badge_background": [255, 255, 255, 255], "border_field_light": [180, 180, 180, 255], - "border_main_light": [212, 212, 212, 255] + "border_main_light": [212, 212, 212, 255], + + "paint_normal_area": "background_3", + "paint_preferred_area": "um_green_5", + "paint_avoid_area": "um_red_5" }, "sizes": { From 425a167391252c41b920955a77ecf38a6f94f4bd Mon Sep 17 00:00:00 2001 From: Sam Bonnekamp Date: Thu, 19 Jun 2025 15:28:49 +0800 Subject: [PATCH 058/159] added 32px cura thumbnail to start gcode --- resources/definitions/anycubic_kobra3v2.def.json | 2 +- resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/anycubic_kobra3v2.def.json b/resources/definitions/anycubic_kobra3v2.def.json index 0f32de2604..143d08b92b 100644 --- a/resources/definitions/anycubic_kobra3v2.def.json +++ b/resources/definitions/anycubic_kobra3v2.def.json @@ -26,7 +26,7 @@ "machine_heated_bed": { "default_value": true}, "machine_center_is_zero": { "default_value": false }, "machine_start_gcode_first": { "default_value": true }, - "machine_start_gcode": { "default_value": "; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\n;TYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" }, + "machine_start_gcode": { "default_value": "; thumbnail begin 32x32\n; iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAX\n; NSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAARWSURBVHgB1Vc7bx1FFD5n/cC+YOQ4RBTXlg0STfIH\n; gM78h0SAhGgQjyqARO8eFEQFBaKhCHIoKagcOkJJQUWBI19XJLaJgp+5M8x5zZzdO+sg0ZCR1zM7u3\n; e+73znMbMAT0KLMVqPdKVhvuze+o2NjUafget7Gz4GeO3s7Ow29YiYiXSJ2TPft0BwEmZ2dpYnp+Ec\n; 8NPT09tpyOAhhF4CXSBPwhOvEZk+B/ynNFzd2QvwxlfHMEq9PqR//mVaWcegmqKCIf9FKGS2Px30Ey\n; A/Hh8fE/gWgRPomww+VkxVgYFoWcz3xXpUJo2QsXvEFu8qAQJvmobA13b3I1u+Q+AhQuRfB1kTBDS6\n; FUVmVBx5iwmqNIgNv9ONkUzg4ODgBQNn2b80yyNbCzFQqMsC4MCNg/eveSQGdQM6glhXYDAYbJHvRw\n; q+ux90dQ8eeBxacaD+5T5ZSdLbfIcT61hTgCZTuq1R/zqDjxMOva6SKzBdMQRoO1OsovcLZrpPkmOO\n; ytibCdPejOQCGJHl5l+zlEGNxBiuvjxI1zNwZTgDzw4a6LbV6/fTT8eqSGD/G3hVgTQRT05OOv6Nyl\n; zBwxgW5iN8/e4leOWlOTivESjAlNhvYJoEvTEA6gq7wIqMyZ76zevPw5XlWdi5/wi+2foLNu88hAeH\n; QQKQJG/ScjjFY1cUFD3mYKxmgeef+1z9AstO4KMEfu3zXdi594gXlHIj3hYFY6su5fVQU7OjgAvZNg\n; kpaDFn8jvrC9zf+GEvgZ+16HqX5QRVctl6FmAyXhq3TmHnROBhWuBysp7aj7/+rS+IWybKO8oPWTns\n; zFX2kxalEKTe+6pZXR9kx0X3olhXdmqJ+GIF6lyfC8ArUCnZ8PPvJ9xfXn4KLMCkuBlo306payJMWD\n; 9BoGspE9LrVop4ajfevgQrz82U5xkQyg7onyEWfpUzxSQBlUnjWRZM8t66cwi/jU5h5eIMbH40hKuv\n; LqS6UFwh7mhv1RL4AYojY9VQbqkQRWL34icPtQ5o6dXqR9fwQlLgrYuPLUSrH96TvZDigsg1UwxFc3\n; c/e5rfsRNRI/JFNNns8MBy8QKNvJYKzGgf4NoXf8LH3+5xTDw4jP0szH16ZvBlybdcitMhpJzrwKUB\n; jcmCiDkDvv/liK92qRWLeTdsZYSsQaPhhXOOZD44uLJhqXAIxYKIQUIaSqBGg+Di09CuBiUYxQ0E/t\n; 0Hc2bk9gQB26kiYJEK7a6kJyqdvL/bKSiKOmx9dGumfpnA35/jnsDH4/H6BAH7Qba3dobTTQYrOY0N\n; usOnbr9JieEiCPgSzzH4/Pz8dpVASw3T2OY0Y2V7FkBrdjDOarErkLPmpgNPkb/u5e8l4CqHG1IqhU\n; xEoQuZztlwuIRw8705WFma8uB3a0jcUhb05pQ/zdp+Ufso8R8hdLqygDs6OnptcXHxD+gx9V8R6H6a\n; dQ8WrSwq81XZ/1NToLztuY9T8D3U687/r/0D2siIlZoKRzIAAAAASUVORK5CYII=\n; thumbnail end\n; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\n;TYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" }, "machine_end_gcode": { "default_value": "G1 X5 Y{machine_depth*0.95} F{speed_travel*60} ; present print\nM140 S0 ; turn off heat bed\nM104 S0 ; turn off temperature\nM84; disable motors ; disable stepper motors" }, "material_bed_temperature": diff --git a/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json b/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json index b85fb3c0ed..6e6fb31e2a 100644 --- a/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json +++ b/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json @@ -38,7 +38,7 @@ "material_print_temp_wait": { "value": true }, "machine_center_is_zero": { "default_value": false }, "machine_start_gcode_first": { "default_value": true }, - "machine_start_gcode": { "default_value": "; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\n;TYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" }, + "machine_start_gcode": { "default_value": "; thumbnail begin 32x32\n; iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAX\n; NSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAARWSURBVHgB1Vc7bx1FFD5n/cC+YOQ4RBTXlg0STfIH\n; gM78h0SAhGgQjyqARO8eFEQFBaKhCHIoKagcOkJJQUWBI19XJLaJgp+5M8x5zZzdO+sg0ZCR1zM7u3\n; e+73znMbMAT0KLMVqPdKVhvuze+o2NjUafget7Gz4GeO3s7Ow29YiYiXSJ2TPft0BwEmZ2dpYnp+Ec\n; 8NPT09tpyOAhhF4CXSBPwhOvEZk+B/ynNFzd2QvwxlfHMEq9PqR//mVaWcegmqKCIf9FKGS2Px30Ey\n; A/Hh8fE/gWgRPomww+VkxVgYFoWcz3xXpUJo2QsXvEFu8qAQJvmobA13b3I1u+Q+AhQuRfB1kTBDS6\n; FUVmVBx5iwmqNIgNv9ONkUzg4ODgBQNn2b80yyNbCzFQqMsC4MCNg/eveSQGdQM6glhXYDAYbJHvRw\n; q+ux90dQ8eeBxacaD+5T5ZSdLbfIcT61hTgCZTuq1R/zqDjxMOva6SKzBdMQRoO1OsovcLZrpPkmOO\n; ytibCdPejOQCGJHl5l+zlEGNxBiuvjxI1zNwZTgDzw4a6LbV6/fTT8eqSGD/G3hVgTQRT05OOv6Nyl\n; zBwxgW5iN8/e4leOWlOTivESjAlNhvYJoEvTEA6gq7wIqMyZ76zevPw5XlWdi5/wi+2foLNu88hAeH\n; QQKQJG/ScjjFY1cUFD3mYKxmgeef+1z9AstO4KMEfu3zXdi594gXlHIj3hYFY6su5fVQU7OjgAvZNg\n; kpaDFn8jvrC9zf+GEvgZ+16HqX5QRVctl6FmAyXhq3TmHnROBhWuBysp7aj7/+rS+IWybKO8oPWTns\n; zFX2kxalEKTe+6pZXR9kx0X3olhXdmqJ+GIF6lyfC8ArUCnZ8PPvJ9xfXn4KLMCkuBlo306payJMWD\n; 9BoGspE9LrVop4ajfevgQrz82U5xkQyg7onyEWfpUzxSQBlUnjWRZM8t66cwi/jU5h5eIMbH40hKuv\n; LqS6UFwh7mhv1RL4AYojY9VQbqkQRWL34icPtQ5o6dXqR9fwQlLgrYuPLUSrH96TvZDigsg1UwxFc3\n; c/e5rfsRNRI/JFNNns8MBy8QKNvJYKzGgf4NoXf8LH3+5xTDw4jP0szH16ZvBlybdcitMhpJzrwKUB\n; jcmCiDkDvv/liK92qRWLeTdsZYSsQaPhhXOOZD44uLJhqXAIxYKIQUIaSqBGg+Di09CuBiUYxQ0E/t\n; 0Hc2bk9gQB26kiYJEK7a6kJyqdvL/bKSiKOmx9dGumfpnA35/jnsDH4/H6BAH7Qba3dobTTQYrOY0N\n; usOnbr9JieEiCPgSzzH4/Pz8dpVASw3T2OY0Y2V7FkBrdjDOarErkLPmpgNPkb/u5e8l4CqHG1IqhU\n; xEoQuZztlwuIRw8705WFma8uB3a0jcUhb05pQ/zdp+Ufso8R8hdLqygDs6OnptcXHxD+gx9V8R6H6a\n; dQ8WrSwq81XZ/1NToLztuY9T8D3U687/r/0D2siIlZoKRzIAAAAASUVORK5CYII=\n; thumbnail end\n; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\n;TYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" }, "machine_end_gcode": { "default_value": "G1 X5 Y{machine_depth*0.95} F{speed_travel*60} ; present print\nM140 S0 ; turn off heat bed\nM104 S0 ; turn off temperature\nM84; disable motors ; disable stepper motors" }, "material_diameter": { "default_value": 1.75 }, From 960d2a231592930cf880cebff66fbd98dfef99b9 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 19 Jun 2025 10:15:37 +0200 Subject: [PATCH 059/159] Optimized application of stroke CURA-12566 --- plugins/PaintTool/PaintView.py | 36 +++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index 71bf6cb014..32124872c4 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -2,6 +2,7 @@ # 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.QtGui import QImage, QColor, QPainter @@ -85,22 +86,35 @@ class PaintView(View): bit_range_start, bit_range_end = self._current_bits_ranges set_value = self._paint_modes[self._current_paint_type].types[brush_color].value << self._current_bits_ranges[0] - clear_mask = 0xffffffff ^ (((0xffffffff << (32 - 1 - (bit_range_end - bit_range_start))) & 0xffffffff) >> (32 - 1 - bit_range_end)) + 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_image.width(), stroke_image.height()) - for x in range(stroke_image.width()): - for y in range(stroke_image.height()): - stroke_pixel = stroke_image.pixel(x, y) - actual_pixel = actual_image.pixel(start_x + x, start_y + y) - if stroke_pixel != 0: - new_pixel = (actual_pixel & clear_mask) | set_value - else: - new_pixel = actual_pixel - stroke_image.setPixel(x, y, new_pixel) + clear_bits_image = stroke_image.copy() + clear_bits_image.invertPixels() + painter = QPainter(clear_bits_image) + painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Lighten) + painter.fillRect(image_rect, clear_mask) + painter.end() + + set_value_image = stroke_image.copy() + painter = QPainter(set_value_image) + painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Multiply) + painter.fillRect(image_rect, set_value) + painter.end() + + stroked_image = actual_image.copy(start_x, start_y, stroke_image.width(), stroke_image.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(stroke_image, start_x, start_y)) + 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)) From 3c04680c71b1f554c4504d9a939c9ff9ae5349cf Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 19 Jun 2025 15:57:07 +0200 Subject: [PATCH 060/159] Load and save texture data mapping in 3MF CURA-12566 --- plugins/3MFReader/ThreeMFReader.py | 23 ++++++++++++++++++++--- plugins/3MFWriter/ThreeMFWriter.py | 12 ++++++++---- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index 4275fbc7b5..aae6ea56ae 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -1,13 +1,14 @@ # Copyright (c) 2021-2022 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. - +import json import os.path import zipfile from typing import List, Optional, Union, TYPE_CHECKING, cast import pySavitar as Savitar import numpy -from PyQt6.QtGui import QImage +from PyQt6.QtCore import QBuffer +from PyQt6.QtGui import QImage, QImageReader from UM.Logger import Logger from UM.Math.Matrix import Matrix @@ -232,11 +233,27 @@ class ThreeMFReader(MeshReader): if texture_path != "" and archive is not None: texture_data = archive.open(texture_path).read() - texture_image = QImage.fromData(texture_data, "PNG") + texture_buffer = QBuffer() + texture_buffer.open(QBuffer.OpenModeFlag.ReadWrite) + texture_buffer.write(texture_data) + + image_reader = QImageReader(texture_buffer, b"png") + + texture_buffer.seek(0) + texture_image = image_reader.read() texture = Texture(OpenGL.getInstance()) texture.setImage(texture_image) sliceable_decorator.setPaintTexture(texture) + texture_buffer.seek(0) + data_mapping_desc = image_reader.text("Description") + if data_mapping_desc != "": + data_mapping = json.loads(data_mapping_desc) + for key, value in data_mapping.items(): + # Tuples are stored as lists in json, restore them back to tuples + data_mapping[key] = tuple(value) + sliceable_decorator.setTextureDataMapping(data_mapping) + return um_node def _read(self, file_name: str) -> Union[SceneNode, List[SceneNode]]: diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index 4cb7840841..9d35f88e6d 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -29,7 +29,7 @@ from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Snapshot import Snapshot from PyQt6.QtCore import Qt, QBuffer -from PyQt6.QtGui import QImage, QPainter +from PyQt6.QtGui import QImage, QPainter, QImageWriter import pySavitar as Savitar from .UCPDialog import UCPDialog @@ -163,12 +163,16 @@ class ThreeMFWriter(MeshWriter): if texture is not None and archive is not None and uv_coordinates_array is not None and len(uv_coordinates_array) > 0: texture_image = texture.getImage() if texture_image is not None: - texture_path = f"{TEXTURES_PATH}/{id(um_node)}.png" - texture_buffer = QBuffer() texture_buffer.open(QBuffer.OpenModeFlag.ReadWrite) - texture_image.save(texture_buffer, "PNG") + image_writer = QImageWriter(texture_buffer, b"png") + texture_data_mapping = um_node.callDecoration("getTextureDataMapping") + if texture_data_mapping is not None: + image_writer.setText("Description", json.dumps(texture_data_mapping)) + image_writer.write(texture_image) + + texture_path = f"{TEXTURES_PATH}/{id(um_node)}.png" texture_file = zipfile.ZipInfo(texture_path) # Don't try to compress texture file, because the PNG is pretty much as compact as it will get archive.writestr(texture_file, texture_buffer.data()) From 8af5514200fb8c7c3b22ec62f2e5d732cb9162b8 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Thu, 19 Jun 2025 22:06:21 +0200 Subject: [PATCH 061/159] Update normal.inst.cfg Quality does not have any values The layer height defaults to 0.2 which makes it identical to other global quality profile for draft resources/quality/draft.inst.cfg --- resources/quality/normal.inst.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/quality/normal.inst.cfg b/resources/quality/normal.inst.cfg index 4ca290b0b5..71b9e92ae3 100644 --- a/resources/quality/normal.inst.cfg +++ b/resources/quality/normal.inst.cfg @@ -11,4 +11,4 @@ type = quality weight = 0 [values] - +layer_height = 0.1 From c10cedc0b9a019d5bf3a629f90bfc313ce715bc7 Mon Sep 17 00:00:00 2001 From: HellAholic <28710690+HellAholic@users.noreply.github.com> Date: Thu, 19 Jun 2025 20:07:49 +0000 Subject: [PATCH 062/159] Apply printer-linter format --- resources/quality/normal.inst.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/quality/normal.inst.cfg b/resources/quality/normal.inst.cfg index 71b9e92ae3..5d82bf612c 100644 --- a/resources/quality/normal.inst.cfg +++ b/resources/quality/normal.inst.cfg @@ -12,3 +12,4 @@ weight = 0 [values] layer_height = 0.1 + From 95f35275be464edf537a4abb76bd16c04f95cd60 Mon Sep 17 00:00:00 2001 From: HellAholic <28710690+HellAholic@users.noreply.github.com> Date: Fri, 20 Jun 2025 07:22:21 +0000 Subject: [PATCH 063/159] Apply printer-linter format --- .../definitions/anycubic_kobra3v2.def.json | 52 ++++++------- .../anycubic_kobra3v2_ACE_PRO.def.json | 77 +++++++++---------- ...ycubic_kobra3v2_ACEPRO_extruder_0.def.json | 2 +- ...ycubic_kobra3v2_ACEPRO_extruder_1.def.json | 2 +- ...ycubic_kobra3v2_ACEPRO_extruder_2.def.json | 2 +- ...ycubic_kobra3v2_ACEPRO_extruder_3.def.json | 2 +- .../anycubic_kobra3v2_extruder_0.def.json | 2 +- 7 files changed, 68 insertions(+), 71 deletions(-) diff --git a/resources/definitions/anycubic_kobra3v2.def.json b/resources/definitions/anycubic_kobra3v2.def.json index 143d08b92b..6b8df0cc4b 100644 --- a/resources/definitions/anycubic_kobra3v2.def.json +++ b/resources/definitions/anycubic_kobra3v2.def.json @@ -8,34 +8,35 @@ "author": "Sam Bonnekamp", "manufacturer": "Anycubic", "file_formats": "text/x-gcode", - "has_textured_buildplate": true, "platform": "anycubic_kobra3v2_buildplate.stl", - "machine_extruder_trains": { "0": "anycubic_kobra3v2_extruder_0" } + "has_textured_buildplate": true, + "machine_extruder_trains": { "0": "anycubic_kobra3v2_extruder_0" } }, "overrides": { + "adhesion_type": { "value": "'skirt'" }, + "layer_height": { "default_value": 0.2 }, + "machine_buildplate_type": { "default_value": "PEI Spring Steel" }, + "machine_center_is_zero": { "default_value": false }, + "machine_depth": { "default_value": 250 }, + "machine_end_gcode": { "default_value": "G1 X5 Y{machine_depth*0.95} F{speed_travel*60} ; present print\nM140 S0 ; turn off heat bed\nM104 S0 ; turn off temperature\nM84; disable motors ; disable stepper motors" }, + "machine_heated_bed": { "default_value": true }, "machine_height": { "default_value": 260 }, - "machine_width": { "default_value": 250 }, - "machine_depth": { "default_value": 250 }, - "machine_name": - { - "description": "Anycubic Kobra 3 v2", - "default_value": "Anycubic Kobra 3 v2" + "machine_name": + { + "default_value": "Anycubic Kobra 3 v2", + "description": "Anycubic Kobra 3 v2" }, - "machine_buildplate_type": { "default_value": "PEI Spring Steel" }, - "machine_heated_bed": { "default_value": true}, - "machine_center_is_zero": { "default_value": false }, - "machine_start_gcode_first": { "default_value": true }, - "machine_start_gcode": { "default_value": "; thumbnail begin 32x32\n; iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAX\n; NSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAARWSURBVHgB1Vc7bx1FFD5n/cC+YOQ4RBTXlg0STfIH\n; gM78h0SAhGgQjyqARO8eFEQFBaKhCHIoKagcOkJJQUWBI19XJLaJgp+5M8x5zZzdO+sg0ZCR1zM7u3\n; e+73znMbMAT0KLMVqPdKVhvuze+o2NjUafget7Gz4GeO3s7Ow29YiYiXSJ2TPft0BwEmZ2dpYnp+Ec\n; 8NPT09tpyOAhhF4CXSBPwhOvEZk+B/ynNFzd2QvwxlfHMEq9PqR//mVaWcegmqKCIf9FKGS2Px30Ey\n; A/Hh8fE/gWgRPomww+VkxVgYFoWcz3xXpUJo2QsXvEFu8qAQJvmobA13b3I1u+Q+AhQuRfB1kTBDS6\n; FUVmVBx5iwmqNIgNv9ONkUzg4ODgBQNn2b80yyNbCzFQqMsC4MCNg/eveSQGdQM6glhXYDAYbJHvRw\n; q+ux90dQ8eeBxacaD+5T5ZSdLbfIcT61hTgCZTuq1R/zqDjxMOva6SKzBdMQRoO1OsovcLZrpPkmOO\n; ytibCdPejOQCGJHl5l+zlEGNxBiuvjxI1zNwZTgDzw4a6LbV6/fTT8eqSGD/G3hVgTQRT05OOv6Nyl\n; zBwxgW5iN8/e4leOWlOTivESjAlNhvYJoEvTEA6gq7wIqMyZ76zevPw5XlWdi5/wi+2foLNu88hAeH\n; QQKQJG/ScjjFY1cUFD3mYKxmgeef+1z9AstO4KMEfu3zXdi594gXlHIj3hYFY6su5fVQU7OjgAvZNg\n; kpaDFn8jvrC9zf+GEvgZ+16HqX5QRVctl6FmAyXhq3TmHnROBhWuBysp7aj7/+rS+IWybKO8oPWTns\n; zFX2kxalEKTe+6pZXR9kx0X3olhXdmqJ+GIF6lyfC8ArUCnZ8PPvJ9xfXn4KLMCkuBlo306payJMWD\n; 9BoGspE9LrVop4ajfevgQrz82U5xkQyg7onyEWfpUzxSQBlUnjWRZM8t66cwi/jU5h5eIMbH40hKuv\n; LqS6UFwh7mhv1RL4AYojY9VQbqkQRWL34icPtQ5o6dXqR9fwQlLgrYuPLUSrH96TvZDigsg1UwxFc3\n; c/e5rfsRNRI/JFNNns8MBy8QKNvJYKzGgf4NoXf8LH3+5xTDw4jP0szH16ZvBlybdcitMhpJzrwKUB\n; jcmCiDkDvv/liK92qRWLeTdsZYSsQaPhhXOOZD44uLJhqXAIxYKIQUIaSqBGg+Di09CuBiUYxQ0E/t\n; 0Hc2bk9gQB26kiYJEK7a6kJyqdvL/bKSiKOmx9dGumfpnA35/jnsDH4/H6BAH7Qba3dobTTQYrOY0N\n; usOnbr9JieEiCPgSzzH4/Pz8dpVASw3T2OY0Y2V7FkBrdjDOarErkLPmpgNPkb/u5e8l4CqHG1IqhU\n; xEoQuZztlwuIRw8705WFma8uB3a0jcUhb05pQ/zdp+Ufso8R8hdLqygDs6OnptcXHxD+gx9V8R6H6a\n; dQ8WrSwq81XZ/1NToLztuY9T8D3U687/r/0D2siIlZoKRzIAAAAASUVORK5CYII=\n; thumbnail end\n; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\n;TYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" }, - "machine_end_gcode": { "default_value": "G1 X5 Y{machine_depth*0.95} F{speed_travel*60} ; present print\nM140 S0 ; turn off heat bed\nM104 S0 ; turn off temperature\nM84; disable motors ; disable stepper motors" }, - - "material_bed_temperature": - { "maximum_value": "110", - "maximum_value_warning": "90" - }, - "material_print_temperature":{ "maximum_value": 300 }, - "material_diameter": { "default_value": 1.75 }, - "material_initial_print_temperature": + "machine_start_gcode": { "default_value": "; thumbnail begin 32x32\n; iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAX\n; NSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAARWSURBVHgB1Vc7bx1FFD5n/cC+YOQ4RBTXlg0STfIH\n; gM78h0SAhGgQjyqARO8eFEQFBaKhCHIoKagcOkJJQUWBI19XJLaJgp+5M8x5zZzdO+sg0ZCR1zM7u3\n; e+73znMbMAT0KLMVqPdKVhvuze+o2NjUafget7Gz4GeO3s7Ow29YiYiXSJ2TPft0BwEmZ2dpYnp+Ec\n; 8NPT09tpyOAhhF4CXSBPwhOvEZk+B/ynNFzd2QvwxlfHMEq9PqR//mVaWcegmqKCIf9FKGS2Px30Ey\n; A/Hh8fE/gWgRPomww+VkxVgYFoWcz3xXpUJo2QsXvEFu8qAQJvmobA13b3I1u+Q+AhQuRfB1kTBDS6\n; FUVmVBx5iwmqNIgNv9ONkUzg4ODgBQNn2b80yyNbCzFQqMsC4MCNg/eveSQGdQM6glhXYDAYbJHvRw\n; q+ux90dQ8eeBxacaD+5T5ZSdLbfIcT61hTgCZTuq1R/zqDjxMOva6SKzBdMQRoO1OsovcLZrpPkmOO\n; ytibCdPejOQCGJHl5l+zlEGNxBiuvjxI1zNwZTgDzw4a6LbV6/fTT8eqSGD/G3hVgTQRT05OOv6Nyl\n; zBwxgW5iN8/e4leOWlOTivESjAlNhvYJoEvTEA6gq7wIqMyZ76zevPw5XlWdi5/wi+2foLNu88hAeH\n; QQKQJG/ScjjFY1cUFD3mYKxmgeef+1z9AstO4KMEfu3zXdi594gXlHIj3hYFY6su5fVQU7OjgAvZNg\n; kpaDFn8jvrC9zf+GEvgZ+16HqX5QRVctl6FmAyXhq3TmHnROBhWuBysp7aj7/+rS+IWybKO8oPWTns\n; zFX2kxalEKTe+6pZXR9kx0X3olhXdmqJ+GIF6lyfC8ArUCnZ8PPvJ9xfXn4KLMCkuBlo306payJMWD\n; 9BoGspE9LrVop4ajfevgQrz82U5xkQyg7onyEWfpUzxSQBlUnjWRZM8t66cwi/jU5h5eIMbH40hKuv\n; LqS6UFwh7mhv1RL4AYojY9VQbqkQRWL34icPtQ5o6dXqR9fwQlLgrYuPLUSrH96TvZDigsg1UwxFc3\n; c/e5rfsRNRI/JFNNns8MBy8QKNvJYKzGgf4NoXf8LH3+5xTDw4jP0szH16ZvBlybdcitMhpJzrwKUB\n; jcmCiDkDvv/liK92qRWLeTdsZYSsQaPhhXOOZD44uLJhqXAIxYKIQUIaSqBGg+Di09CuBiUYxQ0E/t\n; 0Hc2bk9gQB26kiYJEK7a6kJyqdvL/bKSiKOmx9dGumfpnA35/jnsDH4/H6BAH7Qba3dobTTQYrOY0N\n; usOnbr9JieEiCPgSzzH4/Pz8dpVASw3T2OY0Y2V7FkBrdjDOarErkLPmpgNPkb/u5e8l4CqHG1IqhU\n; xEoQuZztlwuIRw8705WFma8uB3a0jcUhb05pQ/zdp+Ufso8R8hdLqygDs6OnptcXHxD+gx9V8R6H6a\n; dQ8WrSwq81XZ/1NToLztuY9T8D3U687/r/0D2siIlZoKRzIAAAAASUVORK5CYII=\n; thumbnail end\n; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\n;TYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" }, + "machine_start_gcode_first": { "default_value": true }, + "machine_width": { "default_value": 250 }, + "material_bed_temperature": + { + "maximum_value": "110", + "maximum_value_warning": "90" + }, + "material_diameter": { "default_value": 1.75 }, + "material_initial_print_temperature": { "maximum_value_warning": 295, "value": "material_print_temperature + 5" @@ -46,9 +47,6 @@ "maximum_value_warning": 295, "value": "material_print_temperature + 5" }, - - "layer_height": { "default_value": 0.2 }, - "adhesion_type": { "value": "'skirt'" }, - "relative_extrusion" : {"value": true} + "relative_extrusion": { "value": true } } -} +} \ No newline at end of file diff --git a/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json b/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json index 6e6fb31e2a..fc464c9eee 100644 --- a/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json +++ b/resources/definitions/anycubic_kobra3v2_ACE_PRO.def.json @@ -8,55 +8,54 @@ "author": "Sam Bonnekamp", "manufacturer": "Anycubic", "file_formats": "text/x-gcode", - "has_textured_buildplate": true, "platform": "anycubic_kobra3v2_buildplate.stl", - "machine_extruder_trains": { - "0": "anycubic_kobra3v2_ACEPRO_extruder_0", - "1": "anycubic_kobra3v2_ACEPRO_extruder_1", - "2": "anycubic_kobra3v2_ACEPRO_extruder_2", - "3": "anycubic_kobra3v2_ACEPRO_extruder_3" - } + "has_textured_buildplate": true, + "machine_extruder_trains": + { + "0": "anycubic_kobra3v2_ACEPRO_extruder_0", + "1": "anycubic_kobra3v2_ACEPRO_extruder_1", + "2": "anycubic_kobra3v2_ACEPRO_extruder_2", + "3": "anycubic_kobra3v2_ACEPRO_extruder_3" + } }, "overrides": { - "machine_height": { "default_value": 260 }, - "machine_width": { "default_value": 250 }, - "machine_depth": { "default_value": 250 }, - "machine_name": - { - "description": "Anycubic Kobra 3 v2", - "default_value": "Anycubic Kobra 3 v2" - }, + "adhesion_type": { "value": "'skirt'" }, + "layer_height": { "default_value": 0.2 }, + "machine_buildplate_type": { "default_value": "PEI Spring Steel" }, + "machine_center_is_zero": { "default_value": false }, + "machine_depth": { "default_value": 250 }, + "machine_end_gcode": { "default_value": "G1 X5 Y{machine_depth*0.95} F{speed_travel*60} ; present print\nM140 S0 ; turn off heat bed\nM104 S0 ; turn off temperature\nM84; disable motors ; disable stepper motors" }, "machine_extruder_count": { "default_value": 4 }, - "machine_buildplate_type": { "default_value": "PEI Spring Steel" }, - "machine_heated_bed": { "default_value": true}, - "material_bed_temperature": - { - "maximum_value": "110", - "maximum_value_warning": "90" - }, - "material_print_temp_wait": { "value": true }, - "machine_center_is_zero": { "default_value": false }, - "machine_start_gcode_first": { "default_value": true }, - "machine_start_gcode": { "default_value": "; thumbnail begin 32x32\n; iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAX\n; NSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAARWSURBVHgB1Vc7bx1FFD5n/cC+YOQ4RBTXlg0STfIH\n; gM78h0SAhGgQjyqARO8eFEQFBaKhCHIoKagcOkJJQUWBI19XJLaJgp+5M8x5zZzdO+sg0ZCR1zM7u3\n; e+73znMbMAT0KLMVqPdKVhvuze+o2NjUafget7Gz4GeO3s7Ow29YiYiXSJ2TPft0BwEmZ2dpYnp+Ec\n; 8NPT09tpyOAhhF4CXSBPwhOvEZk+B/ynNFzd2QvwxlfHMEq9PqR//mVaWcegmqKCIf9FKGS2Px30Ey\n; A/Hh8fE/gWgRPomww+VkxVgYFoWcz3xXpUJo2QsXvEFu8qAQJvmobA13b3I1u+Q+AhQuRfB1kTBDS6\n; FUVmVBx5iwmqNIgNv9ONkUzg4ODgBQNn2b80yyNbCzFQqMsC4MCNg/eveSQGdQM6glhXYDAYbJHvRw\n; q+ux90dQ8eeBxacaD+5T5ZSdLbfIcT61hTgCZTuq1R/zqDjxMOva6SKzBdMQRoO1OsovcLZrpPkmOO\n; ytibCdPejOQCGJHl5l+zlEGNxBiuvjxI1zNwZTgDzw4a6LbV6/fTT8eqSGD/G3hVgTQRT05OOv6Nyl\n; zBwxgW5iN8/e4leOWlOTivESjAlNhvYJoEvTEA6gq7wIqMyZ76zevPw5XlWdi5/wi+2foLNu88hAeH\n; QQKQJG/ScjjFY1cUFD3mYKxmgeef+1z9AstO4KMEfu3zXdi594gXlHIj3hYFY6su5fVQU7OjgAvZNg\n; kpaDFn8jvrC9zf+GEvgZ+16HqX5QRVctl6FmAyXhq3TmHnROBhWuBysp7aj7/+rS+IWybKO8oPWTns\n; zFX2kxalEKTe+6pZXR9kx0X3olhXdmqJ+GIF6lyfC8ArUCnZ8PPvJ9xfXn4KLMCkuBlo306payJMWD\n; 9BoGspE9LrVop4ajfevgQrz82U5xkQyg7onyEWfpUzxSQBlUnjWRZM8t66cwi/jU5h5eIMbH40hKuv\n; LqS6UFwh7mhv1RL4AYojY9VQbqkQRWL34icPtQ5o6dXqR9fwQlLgrYuPLUSrH96TvZDigsg1UwxFc3\n; c/e5rfsRNRI/JFNNns8MBy8QKNvJYKzGgf4NoXf8LH3+5xTDw4jP0szH16ZvBlybdcitMhpJzrwKUB\n; jcmCiDkDvv/liK92qRWLeTdsZYSsQaPhhXOOZD44uLJhqXAIxYKIQUIaSqBGg+Di09CuBiUYxQ0E/t\n; 0Hc2bk9gQB26kiYJEK7a6kJyqdvL/bKSiKOmx9dGumfpnA35/jnsDH4/H6BAH7Qba3dobTTQYrOY0N\n; usOnbr9JieEiCPgSzzH4/Pz8dpVASw3T2OY0Y2V7FkBrdjDOarErkLPmpgNPkb/u5e8l4CqHG1IqhU\n; xEoQuZztlwuIRw8705WFma8uB3a0jcUhb05pQ/zdp+Ufso8R8hdLqygDs6OnptcXHxD+gx9V8R6H6a\n; dQ8WrSwq81XZ/1NToLztuY9T8D3U687/r/0D2siIlZoKRzIAAAAASUVORK5CYII=\n; thumbnail end\n; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\n;TYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" }, - "machine_end_gcode": { "default_value": "G1 X5 Y{machine_depth*0.95} F{speed_travel*60} ; present print\nM140 S0 ; turn off heat bed\nM104 S0 ; turn off temperature\nM84; disable motors ; disable stepper motors" }, - - "material_diameter": { "default_value": 1.75 }, - "material_initial_print_temperature": - { + "machine_heated_bed": { "default_value": true }, + "machine_height": { "default_value": 260 }, + "machine_name": + { + "default_value": "Anycubic Kobra 3 v2", + "description": "Anycubic Kobra 3 v2" + }, + "machine_start_gcode": { "default_value": "; thumbnail begin 32x32\n; iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAX\n; NSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAARWSURBVHgB1Vc7bx1FFD5n/cC+YOQ4RBTXlg0STfIH\n; gM78h0SAhGgQjyqARO8eFEQFBaKhCHIoKagcOkJJQUWBI19XJLaJgp+5M8x5zZzdO+sg0ZCR1zM7u3\n; e+73znMbMAT0KLMVqPdKVhvuze+o2NjUafget7Gz4GeO3s7Ow29YiYiXSJ2TPft0BwEmZ2dpYnp+Ec\n; 8NPT09tpyOAhhF4CXSBPwhOvEZk+B/ynNFzd2QvwxlfHMEq9PqR//mVaWcegmqKCIf9FKGS2Px30Ey\n; A/Hh8fE/gWgRPomww+VkxVgYFoWcz3xXpUJo2QsXvEFu8qAQJvmobA13b3I1u+Q+AhQuRfB1kTBDS6\n; FUVmVBx5iwmqNIgNv9ONkUzg4ODgBQNn2b80yyNbCzFQqMsC4MCNg/eveSQGdQM6glhXYDAYbJHvRw\n; q+ux90dQ8eeBxacaD+5T5ZSdLbfIcT61hTgCZTuq1R/zqDjxMOva6SKzBdMQRoO1OsovcLZrpPkmOO\n; ytibCdPejOQCGJHl5l+zlEGNxBiuvjxI1zNwZTgDzw4a6LbV6/fTT8eqSGD/G3hVgTQRT05OOv6Nyl\n; zBwxgW5iN8/e4leOWlOTivESjAlNhvYJoEvTEA6gq7wIqMyZ76zevPw5XlWdi5/wi+2foLNu88hAeH\n; QQKQJG/ScjjFY1cUFD3mYKxmgeef+1z9AstO4KMEfu3zXdi594gXlHIj3hYFY6su5fVQU7OjgAvZNg\n; kpaDFn8jvrC9zf+GEvgZ+16HqX5QRVctl6FmAyXhq3TmHnROBhWuBysp7aj7/+rS+IWybKO8oPWTns\n; zFX2kxalEKTe+6pZXR9kx0X3olhXdmqJ+GIF6lyfC8ArUCnZ8PPvJ9xfXn4KLMCkuBlo306payJMWD\n; 9BoGspE9LrVop4ajfevgQrz82U5xkQyg7onyEWfpUzxSQBlUnjWRZM8t66cwi/jU5h5eIMbH40hKuv\n; LqS6UFwh7mhv1RL4AYojY9VQbqkQRWL34icPtQ5o6dXqR9fwQlLgrYuPLUSrH96TvZDigsg1UwxFc3\n; c/e5rfsRNRI/JFNNns8MBy8QKNvJYKzGgf4NoXf8LH3+5xTDw4jP0szH16ZvBlybdcitMhpJzrwKUB\n; jcmCiDkDvv/liK92qRWLeTdsZYSsQaPhhXOOZD44uLJhqXAIxYKIQUIaSqBGg+Di09CuBiUYxQ0E/t\n; 0Hc2bk9gQB26kiYJEK7a6kJyqdvL/bKSiKOmx9dGumfpnA35/jnsDH4/H6BAH7Qba3dobTTQYrOY0N\n; usOnbr9JieEiCPgSzzH4/Pz8dpVASw3T2OY0Y2V7FkBrdjDOarErkLPmpgNPkb/u5e8l4CqHG1IqhU\n; xEoQuZztlwuIRw8705WFma8uB3a0jcUhb05pQ/zdp+Ufso8R8hdLqygDs6OnptcXHxD+gx9V8R6H6a\n; dQ8WrSwq81XZ/1NToLztuY9T8D3U687/r/0D2siIlZoKRzIAAAAASUVORK5CYII=\n; thumbnail end\n; external perimeters extrusion width = 0.42mm\n; perimeters extrusion width = 0.45mm\n; infill extrusion width = 0.45mm\n; solid infill extrusion width = 0.45mm\n; top infill extrusion width = 0.42mm\n; support material extrusion width = 0.42mm\n; first layer extrusion width = 0.50mm\n;TYPE:Custom\nG9111 bedTemp={material_bed_temperature} extruderTemp={material_print_temperature}\nM117 ;display LCD message\nM900 K0.05 ;linear advance factor, ive only seen this set to k0.05\n;START HEADER\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nT0 ;set or report the current extruder or other tool\nM107 ; turn fan off" }, + "machine_start_gcode_first": { "default_value": true }, + "machine_width": { "default_value": 250 }, + "material_bed_temperature": + { + "maximum_value": "110", + "maximum_value_warning": "90" + }, + "material_diameter": { "default_value": 1.75 }, + "material_initial_print_temperature": + { "maximum_value_warning": 295, "value": "material_print_temperature + 5" - }, - "material_print_temperature": { "maximum_value": 300 }, - "material_standby_temperature" : {"default_value": "material_print_temperature"}, + }, + "material_print_temp_wait": { "value": true }, + "material_print_temperature": { "maximum_value": 300 }, "material_print_temperature_layer_0": { "maximum_value_warning": 295, "value": "material_print_temperature + 5" }, - - "layer_height": { "default_value": 0.2 }, - "adhesion_type": { "value": "'skirt'" }, - "relative_extrusion" : {"value": true} + "material_standby_temperature": { "default_value": "material_print_temperature" }, + "relative_extrusion": { "value": true } } -} +} \ No newline at end of file diff --git a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_0.def.json b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_0.def.json index ab4a7d1a68..5537606b12 100644 --- a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_0.def.json +++ b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_0.def.json @@ -13,4 +13,4 @@ "machine_nozzle_size": { "default_value": 0.4 }, "material_diameter": { "default_value": 1.75 } } -} +} \ No newline at end of file diff --git a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_1.def.json b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_1.def.json index 029e7ad2bf..2370427eea 100644 --- a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_1.def.json +++ b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_1.def.json @@ -13,4 +13,4 @@ "machine_nozzle_size": { "default_value": 0.4 }, "material_diameter": { "default_value": 1.75 } } -} +} \ No newline at end of file diff --git a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_2.def.json b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_2.def.json index fb732d5fd4..ae860e5f7a 100644 --- a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_2.def.json +++ b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_2.def.json @@ -13,4 +13,4 @@ "machine_nozzle_size": { "default_value": 0.4 }, "material_diameter": { "default_value": 1.75 } } -} +} \ No newline at end of file diff --git a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_3.def.json b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_3.def.json index 2f64b0532a..fed2c1fc6b 100644 --- a/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_3.def.json +++ b/resources/extruders/anycubic_kobra3v2_ACEPRO_extruder_3.def.json @@ -13,4 +13,4 @@ "machine_nozzle_size": { "default_value": 0.4 }, "material_diameter": { "default_value": 1.75 } } -} +} \ No newline at end of file diff --git a/resources/extruders/anycubic_kobra3v2_extruder_0.def.json b/resources/extruders/anycubic_kobra3v2_extruder_0.def.json index dba5e6e559..f5983cf3fb 100644 --- a/resources/extruders/anycubic_kobra3v2_extruder_0.def.json +++ b/resources/extruders/anycubic_kobra3v2_extruder_0.def.json @@ -13,4 +13,4 @@ "machine_nozzle_size": { "default_value": 0.4 }, "material_diameter": { "default_value": 1.75 } } -} +} \ No newline at end of file From be3d6201425e42cf44a710a52dbc200191b551b5 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Fri, 20 Jun 2025 12:28:51 +0200 Subject: [PATCH 064/159] typo fix --- plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml b/plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml index 133cc0edde..931a4fe9f0 100644 --- a/plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml +++ b/plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml @@ -14,7 +14,7 @@ Cura.RoundedRectangle border.color: enabled ? UM.Theme.getColor("lining") : UM.Theme.getColor("action_button_disabled_border") border.width: UM.Theme.getSize("default_lining").width radius: UM.Theme.getSize("default_radius").width - color: getBackgoundColor() + color: getBackgroundColor() signal clicked() property alias imageSource: projectImage.source property alias projectNameText: displayNameLabel.text @@ -106,4 +106,4 @@ Cura.RoundedRectangle return UM.Theme.getColor("action_button_disabled") } } -} \ No newline at end of file +} From 4001e23d913a84bece91bbd54976adb272627d2d Mon Sep 17 00:00:00 2001 From: HellAholic Date: Fri, 20 Jun 2025 14:08:09 +0200 Subject: [PATCH 065/159] Update Cura.proto Add the types missing --- plugins/CuraEngineBackend/Cura.proto | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/CuraEngineBackend/Cura.proto b/plugins/CuraEngineBackend/Cura.proto index 8018c9186f..cdbe463d81 100644 --- a/plugins/CuraEngineBackend/Cura.proto +++ b/plugins/CuraEngineBackend/Cura.proto @@ -78,10 +78,14 @@ message Polygon { SkirtType = 5; InfillType = 6; SupportInfillType = 7; - MoveUnretractedType = 8; - MoveRetractedType = 9; + MoveUnretracted = 8; + MoveRetracted = 9; SupportInterfaceType = 10; PrimeTowerType = 11; + MoveWhileRetracting = 12; + MoveWhileUnretracting = 13; + StationaryRetractUnretract = 14; + NumPrintFeatureTypes = 15; } Type type = 1; // Type of move bytes points = 2; // The points of the polygon, or two points if only a line segment (Currently only line segments are used) From 436d5a669d4fc2af5cd656795ef06508e971d676 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Fri, 20 Jun 2025 20:39:43 +0200 Subject: [PATCH 066/159] Create find-packages.yml --- .github/workflows/find-packages.yml | 118 ++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 .github/workflows/find-packages.yml diff --git a/.github/workflows/find-packages.yml b/.github/workflows/find-packages.yml new file mode 100644 index 0000000000..693dff9ae8 --- /dev/null +++ b/.github/workflows/find-packages.yml @@ -0,0 +1,118 @@ +name: Conan Package Discovery by Jira Ticket + +on: + workflow_dispatch: + inputs: + jira_ticket_number: + description: 'Jira ticket number for Conan package discovery (e.g., cura_12345)' + required: true + type: string + +jobs: + discover_conan_packages: + runs-on: ubuntu-latest + steps: + - name: Checkout repository code + uses: actions/checkout@v4 + + - name: Validate Jira Ticket Number Format + id: validate_input + run: | + set -eou pipefail + JIRA_TICKET="${{ github.event.inputs.jira_ticket_number }}" + # Regex to validate the format: "cura_" followed by one or more digits. + # The '^' and '$' anchors ensure the entire string matches the pattern. + if [[ ! "$JIRA_TICKET" =~ ^cura_[0-9]+$ ]]; then + # Output an error message that will appear as an annotation in the GitHub Actions UI. + # This provides immediate and clear feedback to the user about the expected format. + echo "::error::Invalid Jira ticket number format. Expected format: cura_# (e.g., cura_12345)." + # Exit with a non-zero status code to fail the workflow immediately. + exit 1 + fi + echo "Jira ticket number '$JIRA_TICKET' is valid." + + - name: Setup Conan Client + uses: conan-io/setup-conan@v1 + + - name: Install jq for JSON parsing + run: sudo apt-get update && sudo apt-get install -y jq + + - name: Discover Conan Packages + id: conan_search + env: + CONAN_USERNAME: ${{ secrets.CONAN_USERNAME }} + CONAN_PASSWORD: ${{ secrets.CONAN_PASSWORD }} + run: | + set -eou pipefail + JIRA_TICKET="${{ github.event.inputs.jira_ticket_number }}" + # Construct the full Conan package reference dynamically. + # The format '5.11.0-alpha.0@ultimaker/cura_#' is based on the user's requirement. + CONAN_PACKAGE_REFERENCE="5.11.0-alpha.0@ultimaker/${JIRA_TICKET}" + echo "Searching for Conan packages matching tag: $CONAN_PACKAGE_REFERENCE" + + # Initialize an empty array to store discovered packages. + # This array will hold Markdown-formatted strings for the summary. + DISCOVERED_PACKAGES=() + + # Get a list of all configured Conan remotes in JSON format. + # Conan 2.x's 'conan remote list --format=json' provides structured output. + # This dynamic approach ensures the workflow adapts to any remote configuration changes. + REMOTES_JSON=$(conan remote list --format=json) + + # Parse the JSON to extract remote names. + # The 'jq -r..name' command extracts the 'name' field from each object in the JSON array. + REMOTE_NAMES=$(echo "$REMOTES_JSON" | jq -r '..name') + + # Iterate through each remote to perform a targeted search for binaries. + # For Conan 2.x, 'conan list' is used for detailed package information. + # To find specific binaries, iteration over individual remotes is necessary. + for REMOTE_NAME in $REMOTE_NAMES; do + echo "Searching remote: $REMOTE_NAME" + # Authenticate with the remote if credentials are provided as secrets. + # This is a security best practice for private remotes. + if [ -n "${CONAN_USERNAME:-}" ] && [ -n "${CONAN_PASSWORD:-}" ]; then + echo "Attempting to log in to remote '$REMOTE_NAME'..." + conan remote login "$REMOTE_NAME" -u "$CONAN_USERNAME" -p "$CONAN_PASSWORD" || echo "Login failed for remote $REMOTE_NAME, continuing without authentication for this remote." + fi + # Execute 'conan list' for the specific package reference on the current remote. + # The '--format=json' flag ensures machine-readable output. + # Redirect stderr to /dev/null to suppress non-critical error messages (e.g., remote not found). + SEARCH_RESULT=$(conan list "$CONAN_PACKAGE_REFERENCE" -r "$REMOTE_NAME" --format=json 2>/dev/null || true) + + # Check if any packages were found in this remote's search result. + # The '.results.items | select(.recipe.id == "$CONAN_PACKAGE_REFERENCE")' + # filters for the exact recipe ID. + # '.packages.id' extracts package IDs if binaries are present. + FOUND_ITEMS=$(echo "$SEARCH_RESULT" | jq -r --arg ref "$CONAN_PACKAGE_REFERENCE" \ + '.results.items | select(.recipe.id == $ref) |.packages.id' 2>/dev/null || true) + + if [ -n "$FOUND_ITEMS" ]; then + # If packages are found, add them to the DISCOVERED_PACKAGES array. + # Format: "- `package/version@user/channel#package_id` (Remote: `remote_name`)" + while IFS= read -r PACKAGE_ID; do + DISCOVERED_PACKAGES+=("- \`${CONAN_PACKAGE_REFERENCE}#${PACKAGE_ID}\` (Remote: \`${REMOTE_NAME}\`)") + done <<< "$FOUND_ITEMS" + else + echo "No packages found in $REMOTE_NAME matching $CONAN_PACKAGE_REFERENCE" + fi + done + + # Prepare the summary content for the GitHub Actions run summary. + # This content will be written to the GITHUB_STEP_SUMMARY file. + echo "### Conan Packages Found for Jira Ticket: ${JIRA_TICKET}" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" # Add a blank line for spacing + echo "The workflow searched for Conan packages tagged \`${CONAN_PACKAGE_REFERENCE}\` across all configured remotes." >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" # Add a blank line for spacing + echo "**Discovered Packages:**" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" # Add a blank line for spacing + + if [ ${#DISCOVERED_PACKAGES[@]} -eq 0 ]; then + echo "*No packages found matching the specified tag.*" >> "$GITHUB_STEP_SUMMARY" + else + # Iterate through the array of discovered packages and append each to the summary file. + for PACKAGE_ENTRY in "${DISCOVERED_PACKAGES[@]}"; do + echo "$PACKAGE_ENTRY" >> "$GITHUB_STEP_SUMMARY" + done + fi + echo "" >> "$GITHUB_STEP_SUMMARY" # Add a blank line for spacing + echo "---" >> "$GITHUB_STEP_SUMMARY" # Add a separator for cleanliness \ No newline at end of file From 4948adf03e199b46351073c893f3e1297e4821e7 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Fri, 20 Jun 2025 21:02:32 +0200 Subject: [PATCH 067/159] Update find-packages.yml --- .github/workflows/find-packages.yml | 113 ++-------------------------- 1 file changed, 6 insertions(+), 107 deletions(-) diff --git a/.github/workflows/find-packages.yml b/.github/workflows/find-packages.yml index 693dff9ae8..e9c60a89b4 100644 --- a/.github/workflows/find-packages.yml +++ b/.github/workflows/find-packages.yml @@ -9,110 +9,9 @@ on: type: string jobs: - discover_conan_packages: - runs-on: ubuntu-latest - steps: - - name: Checkout repository code - uses: actions/checkout@v4 - - - name: Validate Jira Ticket Number Format - id: validate_input - run: | - set -eou pipefail - JIRA_TICKET="${{ github.event.inputs.jira_ticket_number }}" - # Regex to validate the format: "cura_" followed by one or more digits. - # The '^' and '$' anchors ensure the entire string matches the pattern. - if [[ ! "$JIRA_TICKET" =~ ^cura_[0-9]+$ ]]; then - # Output an error message that will appear as an annotation in the GitHub Actions UI. - # This provides immediate and clear feedback to the user about the expected format. - echo "::error::Invalid Jira ticket number format. Expected format: cura_# (e.g., cura_12345)." - # Exit with a non-zero status code to fail the workflow immediately. - exit 1 - fi - echo "Jira ticket number '$JIRA_TICKET' is valid." - - - name: Setup Conan Client - uses: conan-io/setup-conan@v1 - - - name: Install jq for JSON parsing - run: sudo apt-get update && sudo apt-get install -y jq - - - name: Discover Conan Packages - id: conan_search - env: - CONAN_USERNAME: ${{ secrets.CONAN_USERNAME }} - CONAN_PASSWORD: ${{ secrets.CONAN_PASSWORD }} - run: | - set -eou pipefail - JIRA_TICKET="${{ github.event.inputs.jira_ticket_number }}" - # Construct the full Conan package reference dynamically. - # The format '5.11.0-alpha.0@ultimaker/cura_#' is based on the user's requirement. - CONAN_PACKAGE_REFERENCE="5.11.0-alpha.0@ultimaker/${JIRA_TICKET}" - echo "Searching for Conan packages matching tag: $CONAN_PACKAGE_REFERENCE" - - # Initialize an empty array to store discovered packages. - # This array will hold Markdown-formatted strings for the summary. - DISCOVERED_PACKAGES=() - - # Get a list of all configured Conan remotes in JSON format. - # Conan 2.x's 'conan remote list --format=json' provides structured output. - # This dynamic approach ensures the workflow adapts to any remote configuration changes. - REMOTES_JSON=$(conan remote list --format=json) - - # Parse the JSON to extract remote names. - # The 'jq -r..name' command extracts the 'name' field from each object in the JSON array. - REMOTE_NAMES=$(echo "$REMOTES_JSON" | jq -r '..name') - - # Iterate through each remote to perform a targeted search for binaries. - # For Conan 2.x, 'conan list' is used for detailed package information. - # To find specific binaries, iteration over individual remotes is necessary. - for REMOTE_NAME in $REMOTE_NAMES; do - echo "Searching remote: $REMOTE_NAME" - # Authenticate with the remote if credentials are provided as secrets. - # This is a security best practice for private remotes. - if [ -n "${CONAN_USERNAME:-}" ] && [ -n "${CONAN_PASSWORD:-}" ]; then - echo "Attempting to log in to remote '$REMOTE_NAME'..." - conan remote login "$REMOTE_NAME" -u "$CONAN_USERNAME" -p "$CONAN_PASSWORD" || echo "Login failed for remote $REMOTE_NAME, continuing without authentication for this remote." - fi - # Execute 'conan list' for the specific package reference on the current remote. - # The '--format=json' flag ensures machine-readable output. - # Redirect stderr to /dev/null to suppress non-critical error messages (e.g., remote not found). - SEARCH_RESULT=$(conan list "$CONAN_PACKAGE_REFERENCE" -r "$REMOTE_NAME" --format=json 2>/dev/null || true) - - # Check if any packages were found in this remote's search result. - # The '.results.items | select(.recipe.id == "$CONAN_PACKAGE_REFERENCE")' - # filters for the exact recipe ID. - # '.packages.id' extracts package IDs if binaries are present. - FOUND_ITEMS=$(echo "$SEARCH_RESULT" | jq -r --arg ref "$CONAN_PACKAGE_REFERENCE" \ - '.results.items | select(.recipe.id == $ref) |.packages.id' 2>/dev/null || true) - - if [ -n "$FOUND_ITEMS" ]; then - # If packages are found, add them to the DISCOVERED_PACKAGES array. - # Format: "- `package/version@user/channel#package_id` (Remote: `remote_name`)" - while IFS= read -r PACKAGE_ID; do - DISCOVERED_PACKAGES+=("- \`${CONAN_PACKAGE_REFERENCE}#${PACKAGE_ID}\` (Remote: \`${REMOTE_NAME}\`)") - done <<< "$FOUND_ITEMS" - else - echo "No packages found in $REMOTE_NAME matching $CONAN_PACKAGE_REFERENCE" - fi - done - - # Prepare the summary content for the GitHub Actions run summary. - # This content will be written to the GITHUB_STEP_SUMMARY file. - echo "### Conan Packages Found for Jira Ticket: ${JIRA_TICKET}" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" # Add a blank line for spacing - echo "The workflow searched for Conan packages tagged \`${CONAN_PACKAGE_REFERENCE}\` across all configured remotes." >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" # Add a blank line for spacing - echo "**Discovered Packages:**" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" # Add a blank line for spacing - - if [ ${#DISCOVERED_PACKAGES[@]} -eq 0 ]; then - echo "*No packages found matching the specified tag.*" >> "$GITHUB_STEP_SUMMARY" - else - # Iterate through the array of discovered packages and append each to the summary file. - for PACKAGE_ENTRY in "${DISCOVERED_PACKAGES[@]}"; do - echo "$PACKAGE_ENTRY" >> "$GITHUB_STEP_SUMMARY" - done - fi - echo "" >> "$GITHUB_STEP_SUMMARY" # Add a blank line for spacing - echo "---" >> "$GITHUB_STEP_SUMMARY" # Add a separator for cleanliness \ No newline at end of file + find-packages: + name: Find packages for Jira ticket + uses: ultimaker/cura-workflows/.github/workflows/find_package_by_ticket.yml@jira_find_package + with: + jira_ticket_number: ${{ inputs.jira_ticket_number }} + secrets: inherit \ No newline at end of file From 2a45cf3274cd533b3a1cb406c9d54b56d0e6e6fc Mon Sep 17 00:00:00 2001 From: HellAholic Date: Sat, 21 Jun 2025 10:03:49 +0200 Subject: [PATCH 068/159] set_permission set high level permission to none --- .github/workflows/find-packages.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/find-packages.yml b/.github/workflows/find-packages.yml index e9c60a89b4..fdb3d8ebf4 100644 --- a/.github/workflows/find-packages.yml +++ b/.github/workflows/find-packages.yml @@ -8,10 +8,12 @@ on: required: true type: string +permissions: {} + jobs: find-packages: name: Find packages for Jira ticket uses: ultimaker/cura-workflows/.github/workflows/find_package_by_ticket.yml@jira_find_package with: jira_ticket_number: ${{ inputs.jira_ticket_number }} - secrets: inherit \ No newline at end of file + secrets: inherit From 1ca58824acfb1bf06fa170120520eb2d32345cbd Mon Sep 17 00:00:00 2001 From: "Steinar H. Gunderson" Date: Sun, 4 Aug 2024 16:28:23 +0200 Subject: [PATCH 069/159] Add printer definitions for Sovol SV08. The SV08 (or SV-08; nomenclature is not completely consistent) is a relatively new printed based on Voron 2.4, running Klipper. This adds printer, extruder and material definitions for it, based on the voron2_base definitions (by copying, so as to stay independent of voron2_base is changed) plus Sovol's published profiles for Orca Slicer: https://drive.google.com/drive/folders/1KWjLxwpO_9_Xqi_f6qu84HRxZi26a_GN Unfortunately, the included STL model for the platform does not have texture coordinates, so we cannot use the platform texture (unless someone goes to add them manually or otherwise adjusts the model). The following settings were not carried over, mostly because I could not find any obvious equivalent in Cura: - Machine: "retract_before_wipe": [ "0%" ], "machine_max_acceleration_extruding": [ "20000" ], "machine_max_acceleration_retracting": [ "5000" ], "retract_length_toolchange": [ "2" ], "wipe_distance": [ "2" ], "retract_lift_below": [ "343" ], "thumbnails_format": "PNG", "before_layer_change_gcode": "TIMELAPSE_TAKE_FRAME\nG92 E0", - Filament (using ABS as an example; the exact values differ between the four material profiles): "nozzle_temperature_range_low": [ "190" ], "nozzle_temperature_range_high": [ "250" ], "overhang_fan_threshold": [ "25%" ], "temperature_vitrification": [ "60" ], # Only used for arranging. "close_fan_the_first_x_layers": [ "3" ], "full_fan_speed_layer": [ "0" ], # Inconsistent; effectively 4. # Enclosure fan (M106 P3 commands) "activate_air_filtration": [ "1" ], "complete_print_exhaust_fan_speed": [ "60" ], "during_print_exhaust_fan_speed": [ "100" ], - Process: A bunch (e.g. bridge_flow, elephant_foot_compensation, overhang_1_4_speed, etc. etc.), but it's unclear how many are printer-specific and how many are just Orca defaults where Cura wants to do things differently. The start and end G-code are mostly copied over verbatim, except that it leaves the printer in relative coordinate mode and Cura does not set this explicitly back to absolute, so we need an explicit G90 at the end. (Also, there seems to be a Klipper issue where G90 does not reset extrusion to absolute as well, so we need to send an explicit M82.) We give EXTRUDER_TEMP= and BED_TEMP= as parameters to the START_PRINT macro; the Sovol stock macros ignore these, but the popular mainline Klipper installation can use this to e.g. bed mesh at the correct temperature. We also use the new Cura 5.8 conditionals to reduce the extrusion amount for finer nozzles than 0.4mm, as we get Klipper errors otherwise. Unfortunately, Cura chooses SS_ as prefix instead of SV08_. I don't know if there is a way to override this; the other Sovol printers seem to have the same issue. I've tested this with the standard 0.4mm nozzle and ABS/PLA, using the Moonraker plugin. PETG and TPU are untested, in part because the current nozzle is said to be unsafe for PETG. The time estimates from Cura are not all that good, but klipper_estimator helps. (The Klipper object exclusion plugin is also recommended, as it allows the printer to bed mesh a smaller area.) Future work would include supporting the 0.2mm, 0.6mm and 0.8mm nozzles. There are separate profiles for them, with different layer height, support settings, print speeds, etc. -- and then there is a specific PLA/0.2mm profile with lower printing speed and higher fan settings. Also, it would be really good to support the enclosure fan (M106 P3, known as exhaust_fan in Orca) for printing ABS; it's possible that something could be done using the Cura fan control plugin, but it would be better to simply have it right in the filament settings. Similarly, the ABS/PETG profiles want to turn off the fan entirely the first three layers (to improve adhesion), but Cura can only ramp linearly starting from the first layer, not hold the first few layers constant. --- resources/definitions/sovol_sv08.def.json | 131 ++++++++++++++++++ .../extruders/sovol_sv08_extruder.def.json | 19 +++ .../meshes/sovol_sv08_buildplate_model.stl | Bin 0 -> 454884 bytes .../ABS/sovol_sv08_0.4_ABS_standard.inst.cfg | 26 ++++ .../sovol_sv08_0.4_PETG_standard.inst.cfg | 26 ++++ .../PLA/sovol_sv08_0.4_PLA_standard.inst.cfg | 26 ++++ .../TPU/sovol_sv08_0.4_TPU_standard.inst.cfg | 26 ++++ .../quality/sovol/sovol_sv08_global.inst.cfg | 30 ++++ .../variants/sovol/sovol_sv08_0.4.inst.cfg | 13 ++ 9 files changed, 297 insertions(+) create mode 100644 resources/definitions/sovol_sv08.def.json create mode 100644 resources/extruders/sovol_sv08_extruder.def.json create mode 100644 resources/meshes/sovol_sv08_buildplate_model.stl create mode 100644 resources/quality/sovol/ABS/sovol_sv08_0.4_ABS_standard.inst.cfg create mode 100644 resources/quality/sovol/PETG/sovol_sv08_0.4_PETG_standard.inst.cfg create mode 100644 resources/quality/sovol/PLA/sovol_sv08_0.4_PLA_standard.inst.cfg create mode 100644 resources/quality/sovol/TPU/sovol_sv08_0.4_TPU_standard.inst.cfg create mode 100644 resources/quality/sovol/sovol_sv08_global.inst.cfg create mode 100644 resources/variants/sovol/sovol_sv08_0.4.inst.cfg diff --git a/resources/definitions/sovol_sv08.def.json b/resources/definitions/sovol_sv08.def.json new file mode 100644 index 0000000000..52f5fb6138 --- /dev/null +++ b/resources/definitions/sovol_sv08.def.json @@ -0,0 +1,131 @@ +{ + "version": 2, + "name": "Sovol SV08", + "inherits": "fdmprinter", + "metadata": + { + "visible": true, + "author": "See voron2_base", + "manufacturer": "Sovol 3D", + "preferred_variant_name": "0.4mm Nozzle", + "quality_definition": "sovol_sv08", + "variants_name": "Nozzle Size", + "platform": "sovol_sv08_buildplate_model.stl", + "file_formats": "text/x-gcode", + "exclude_materials": [], + "first_start_actions": [ "MachineSettingsAction" ], + "has_machine_quality": true, + "has_materials": true, + "has_variants": true, + "machine_extruder_trains": { "0": "voron2_extruder_0" }, + "preferred_material": "generic_abs", + "preferred_quality_type": "fast" + }, + "overrides": + { + "machine_depth": { "default_value": 350 }, + "machine_width": { "default_value": 350 }, + "machine_height": { "default_value": 345 }, + "machine_name": { "default_value": "SV08" }, + "retraction_amount": { "default_value": 0.5 }, + "machine_max_acceleration_x": { "default_value": 40000 }, + "machine_max_acceleration_y": { "default_value": 40000 }, + "machine_max_acceleration_z": { "default_value": 500 }, + "machine_max_acceleration_e": { "default_value": 5000 }, + "machine_max_feedrate_x": { "default_value": 700 }, + "machine_max_feedrate_y": { "default_value": 700 }, + "machine_max_feedrate_z": { "default_value": 20 }, + "machine_max_feedrate_e": { "default_value": 50 }, + "machine_max_jerk_e": { "default_value": 5 }, + "machine_max_jerk_xy": { "default_value": 20 }, + "machine_max_jerk_z": { "default_value": 0.5 }, + "retraction_min_travel": { "default_value": 1 }, + "retraction_hop": { "default_value": 0.4 }, + "machine_start_gcode": { "default_value": "G28 ; Move to zero\nG90 ; Absolute positioning\nG1 X0 F9000\nG1 Y20\nG1 Z0.600 F600\nG1 Y0 F9000\nSTART_PRINT EXTRUDER_TEMP={material_print_temperature_layer_0} BED_TEMP={material_bed_temperature_layer_0}\nG90 ; Absolute positioning (START_PRINT might have changed it)\nG1 X0 F9000\nG1 Y20\nG1 Z0.600 F600\nG1 Y0 F9000\nM400\nG91 ; Relative positioning\nM83 ; Relative extrusion\nM140 S{material_bed_temperature_layer_0} ; Set bed temp\nM104 S{material_print_temperature_layer_0} ; Set extruder temp\nM190 S{material_bed_temperature_layer_0} ; Wait for bed temp\nM109 S{material_print_temperature_layer_0} ; Wait for extruder temp\n{if machine_nozzle_size >= 0.4}\n; Standard Sovol blob and purge line.\nG1 E25 F300 ; Purge blob\nG4 P1000 ; Wait a bit to let it finish\nG1 E-0.200 Z5 F600 ; Retract\nG1 X88.000 F9000 ; Move right and then down for purge line\nG1 Z-5.000 F600\nG1 X87.000 E20.88 F1800 ; Purge line right\nG1 X87.000 E13.92 F1800\nG1 Y1 E0.16 F1800 ; Small movement back for next line\nG1 X-87.000 E13.92 F1800 ; Purge line left\nG1 X-87.000 E20.88 F1800\nG1 Y1 E0.24 F1800 ; Small movement back for next line\nG1 X87.000 E20.88 F1800 ; Purge line right\nG1 X87.000 E13.92 F1800\nG1 E-0.200 Z1 F600\n{else}\n; The default start G-code uses too high flow for smaller nozzles,\n; which causes Klipper errors. Scale everything back by\n; (0.25/0.4)^2, i.e., for 0.25mm nozzle. This should be good\n; enough for 0.2mm as well.\nG1 E8 F300 ; Purge blob\nG4 P1000 ; Wait a bit to let it finish\nG1 E-0.078 Z5 F600 ; Retract\nG1 X88.000 F9000 ; Move right and then down for purge line\nG1 Z-5.000 F600\nG1 X87.000 E8.16 F1800 ; Purge line right\nG1 X87.000 E5.44 F1800\nG1 Y1 E0.063 F1800 ; Small movement back for next line\nG1 X-87.000 E5.44 F1800 ; Purge line left\nG1 X-87.000 E8.16 F1800\nG1 Y1 E0.094 F1800 ; Small movement back for next line\nG1 X87.000 E8.16 F1800 ; Purge line right\nG1 X87.000 E5.44 F1800\nG1 E-0.078 Z1 F600\n{endif}\nM400 ; Wait for moves to finish\nG90 ; Absolute positioning\nM82 ; Absolute extrusion mode\n" }, + "machine_end_gcode": { "default_value": "END_PRINT\n" }, + "acceleration_enabled": { "default_value": false }, + "acceleration_layer_0": { "value": 1800 }, + "acceleration_print": { "default_value": 2200 }, + "acceleration_roofing": { "value": 1800 }, + "acceleration_travel_layer_0": { "value": 1800 }, + "acceleration_wall_0": { "value": 1800 }, + "adhesion_type": { "default_value": "skirt" }, + "alternate_extra_perimeter": { "default_value": true }, + "bridge_fan_speed": { "default_value": 100 }, + "bridge_fan_speed_2": { "resolve": "max(cool_fan_speed, 50)" }, + "bridge_fan_speed_3": { "resolve": "max(cool_fan_speed, 20)" }, + "bridge_settings_enabled": { "default_value": true }, + "bridge_wall_coast": { "default_value": 10 }, + "cool_fan_full_at_height": { "value": "resolveOrValue('layer_height_0') + resolveOrValue('layer_height') * max(1, cool_fan_full_layer - 1)" }, + "cool_fan_full_layer": { "value": 4 }, + "cool_fan_speed_min": { "value": "cool_fan_speed" }, + "cool_min_layer_time": { "default_value": 15 }, + "cool_min_layer_time_fan_speed_max": { "default_value": 20 }, + "fill_outline_gaps": { "default_value": true }, + "gantry_height": { "value": 30 }, + "infill_before_walls": { "default_value": false }, + "infill_enable_travel_optimization": { "default_value": true }, + "jerk_enabled": { "default_value": false }, + "jerk_roofing": { "value": 10 }, + "jerk_wall_0": { "value": 10 }, + "layer_height_0": { "resolve": "max(0.2, min(extruderValues('layer_height')))" }, + "line_width": { "value": "machine_nozzle_size * 1.125" }, + "machine_acceleration": { "default_value": 1500 }, + "machine_endstop_positive_direction_x": { "default_value": true }, + "machine_endstop_positive_direction_y": { "default_value": true }, + "machine_endstop_positive_direction_z": { "default_value": false }, + "machine_feeder_wheel_diameter": { "default_value": 7.5 }, + "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, + "machine_head_with_fans_polygon": + { + "default_value": [ + [-35, 65], + [-35, -50], + [35, -50], + [35, 65] + ] + }, + "machine_heated_bed": { "default_value": true }, + "machine_steps_per_mm_x": { "default_value": 80 }, + "machine_steps_per_mm_y": { "default_value": 80 }, + "machine_steps_per_mm_z": { "default_value": 400 }, + "meshfix_maximum_resolution": { "default_value": 0.01 }, + "min_infill_area": { "default_value": 5.0 }, + "minimum_polygon_circumference": { "default_value": 0.2 }, + "optimize_wall_printing_order": { "default_value": true }, + "retraction_combing": { "value": "'noskin'" }, + "retraction_combing_max_distance": { "default_value": 10 }, + "retraction_hop_enabled": { "default_value": true }, + "retraction_prime_speed": + { + "maximum_value_warning": 130, + "value": "math.ceil(retraction_speed * 0.4)" + }, + "retraction_retract_speed": { "maximum_value_warning": 130 }, + "retraction_speed": + { + "default_value": 30, + "maximum_value_warning": 130 + }, + "roofing_layer_count": { "value": 1 }, + "skirt_brim_minimal_length": { "default_value": 550 }, + "speed_layer_0": { "value": "math.ceil(speed_print * 0.25)" }, + "speed_roofing": { "value": "math.ceil(speed_print * 0.33)" }, + "speed_slowdown_layers": { "default_value": 4 }, + "speed_topbottom": { "value": "math.ceil(speed_print * 0.33)" }, + "speed_travel": + { + "maximum_value_warning": 501, + "value": 300 + }, + "speed_travel_layer_0": { "value": "math.ceil(speed_travel * 0.4)" }, + "speed_wall": { "value": "math.ceil(speed_print * 0.33)" }, + "speed_wall_0": { "value": "math.ceil(speed_print * 0.33)" }, + "speed_wall_x": { "value": "math.ceil(speed_print * 0.66)" }, + "travel_avoid_other_parts": { "default_value": false }, + "wall_line_width": { "value": "machine_nozzle_size" }, + "wall_overhang_angle": { "default_value": 75 }, + "wall_overhang_speed_factor": { "default_value": 50 }, + "zig_zaggify_infill": { "value": true } + } +} diff --git a/resources/extruders/sovol_sv08_extruder.def.json b/resources/extruders/sovol_sv08_extruder.def.json new file mode 100644 index 0000000000..407c81de47 --- /dev/null +++ b/resources/extruders/sovol_sv08_extruder.def.json @@ -0,0 +1,19 @@ +{ + "version": 2, + "name": "Nozzle Size", + "inherits": "fdmextruder", + "metadata": + { + "machine": "sovol_sv08", + "position": "0" + }, + "overrides": + { + "extruder_nr": + { + "default_value": 0, + "maximum_value": 1 + }, + "material_diameter": { "default_value": 1.75 } + } +} diff --git a/resources/meshes/sovol_sv08_buildplate_model.stl b/resources/meshes/sovol_sv08_buildplate_model.stl new file mode 100644 index 0000000000000000000000000000000000000000..91acb50bd443f13d12c84d80a912e5d5b21ff1d8 GIT binary patch literal 454884 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

8}u(~qva{JL7O*{ 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 z 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

gqjqWHOjVDU%F#pu@`EC^N&2jaCG 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 literal 0 HcmV?d00001 diff --git a/resources/quality/sovol/ABS/sovol_sv08_0.4_ABS_standard.inst.cfg b/resources/quality/sovol/ABS/sovol_sv08_0.4_ABS_standard.inst.cfg new file mode 100644 index 0000000000..2fda8d1811 --- /dev/null +++ b/resources/quality/sovol/ABS/sovol_sv08_0.4_ABS_standard.inst.cfg @@ -0,0 +1,26 @@ +[general] +definition = sovol_sv08 +name = Standard Quality +version = 4 + +[metadata] +material = generic_abs +quality_type = standard +setting_version = 25 +type = quality +variant = 0.4mm Nozzle + +[values] +cool_fan_enabled = True +cool_fan_speed = 10 +cool_fan_speed_max = 30 +cool_min_layer_time_fan_speed_max = 30 +cool_min_layer_time = 4 +cool_min_speed = 10 +material_flow = 98 +material_max_flowrate = 21 +material_print_temperature = 270 +material_print_temperature_layer_0 = 280 +material_bed_temperature = 95 +bridge_settings_enabled = True +bridge_fan_speed = 30 diff --git a/resources/quality/sovol/PETG/sovol_sv08_0.4_PETG_standard.inst.cfg b/resources/quality/sovol/PETG/sovol_sv08_0.4_PETG_standard.inst.cfg new file mode 100644 index 0000000000..ba43c72c8b --- /dev/null +++ b/resources/quality/sovol/PETG/sovol_sv08_0.4_PETG_standard.inst.cfg @@ -0,0 +1,26 @@ +[general] +definition = sovol_sv08 +name = Standard Quality +version = 4 + +[metadata] +material = generic_petg +quality_type = standard +setting_version = 25 +type = quality +variant = 0.4mm Nozzle + +[values] +cool_fan_enabled = True +cool_fan_speed = 10 +cool_fan_speed_max = 30 +cool_min_layer_time_fan_speed_max = 30 +cool_min_layer_time = 5 +cool_min_speed = 10 +material_flow = 98 +material_max_flowrate = 17 +material_print_temperature = 235 +material_print_temperature_layer_0 = 250 +material_bed_temperature = 75 +bridge_settings_enabled = True +bridge_fan_speed = 70 diff --git a/resources/quality/sovol/PLA/sovol_sv08_0.4_PLA_standard.inst.cfg b/resources/quality/sovol/PLA/sovol_sv08_0.4_PLA_standard.inst.cfg new file mode 100644 index 0000000000..2a70b9415b --- /dev/null +++ b/resources/quality/sovol/PLA/sovol_sv08_0.4_PLA_standard.inst.cfg @@ -0,0 +1,26 @@ +[general] +definition = sovol_sv08 +name = Standard Quality +version = 4 + +[metadata] +material = generic_pla +quality_type = standard +setting_version = 25 +type = quality +variant = 0.4mm Nozzle + +[values] +cool_fan_enabled = True +cool_fan_speed = 50 +cool_fan_speed_max = 70 +cool_min_layer_time_fan_speed_max = 50 +cool_min_layer_time = 5 +cool_min_speed = 10 +material_flow = 98 +material_max_flowrate = 21 +material_print_temperature = 220 +material_print_temperature_layer_0 = 235 +material_bed_temperature = 65 +bridge_settings_enabled = True +bridge_fan_speed = 100 diff --git a/resources/quality/sovol/TPU/sovol_sv08_0.4_TPU_standard.inst.cfg b/resources/quality/sovol/TPU/sovol_sv08_0.4_TPU_standard.inst.cfg new file mode 100644 index 0000000000..1908bdb6b0 --- /dev/null +++ b/resources/quality/sovol/TPU/sovol_sv08_0.4_TPU_standard.inst.cfg @@ -0,0 +1,26 @@ +[general] +definition = sovol_sv08 +name = Standard Quality +version = 4 + +[metadata] +material = generic_tpu +quality_type = standard +setting_version = 25 +type = quality +variant = 0.4mm Nozzle + +[values] +cool_fan_enabled = True +cool_fan_speed = 80 +cool_fan_speed_max = 100 +cool_min_layer_time_fan_speed_max = 50 +cool_min_layer_time = 5 +cool_min_speed = 10 +material_flow = 98 +material_max_flowrate = 3.6 +material_print_temperature = 240 +material_print_temperature_layer_0 = 235 +material_bed_temperature = 65 +bridge_settings_enabled = True +bridge_fan_speed = 100 diff --git a/resources/quality/sovol/sovol_sv08_global.inst.cfg b/resources/quality/sovol/sovol_sv08_global.inst.cfg new file mode 100644 index 0000000000..2f2f0004ab --- /dev/null +++ b/resources/quality/sovol/sovol_sv08_global.inst.cfg @@ -0,0 +1,30 @@ +[general] +definition = sovol_sv08 +name = 0.20mm Standard +version = 4 + +[metadata] +global_quality = True +quality_type = standard +setting_version = 23 +type = quality + +[values] +layer_height = 0.2 +speed_print = 600 +speed_wall_x = 300 +speed_wall_0 = 200 +speed_infill = 200 +speed_travel = =speed_print +skirt_brim_speed = 80 +speed_ironing = 15 +speed_layer_0 = 30 +speed_slowdown_layers = 3 +acceleration_enabled = True +acceleration_print = 20000 +acceleration_wall_0 = 8000 +acceleration_wall_x = 12000 +acceleration_roofing = =acceleration_wall_0 +acceleration_topbottom = =acceleration_wall +acceleration_layer_0 = 3000 +acceleration_travel = 40000 diff --git a/resources/variants/sovol/sovol_sv08_0.4.inst.cfg b/resources/variants/sovol/sovol_sv08_0.4.inst.cfg new file mode 100644 index 0000000000..ce8ae1f23e --- /dev/null +++ b/resources/variants/sovol/sovol_sv08_0.4.inst.cfg @@ -0,0 +1,13 @@ +[general] +definition = sovol_sv08 +name = 0.4mm Nozzle +version = 4 + +[metadata] +hardware_type = nozzle +setting_version = 23 +type = variant + +[values] +machine_nozzle_size = 0.4 + From 694ef2a8c9486f37c86a66c95836d9f86b229c63 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Tue, 24 Jun 2025 12:16:05 +0200 Subject: [PATCH 070/159] Update find-packages workflow Add input to start builds Update the reference to the workflow name --- .github/workflows/find-packages.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/find-packages.yml b/.github/workflows/find-packages.yml index fdb3d8ebf4..43c4b6f17e 100644 --- a/.github/workflows/find-packages.yml +++ b/.github/workflows/find-packages.yml @@ -7,13 +7,18 @@ on: description: 'Jira ticket number for Conan package discovery (e.g., cura_12345)' required: true type: string + start_builds: + default: false + required: false + type: boolean permissions: {} jobs: find-packages: name: Find packages for Jira ticket - uses: ultimaker/cura-workflows/.github/workflows/find_package_by_ticket.yml@jira_find_package + uses: ultimaker/cura-workflows/.github/workflows/find-package-by-ticket.yml@jira_find_package with: jira_ticket_number: ${{ inputs.jira_ticket_number }} + start_builds: ${{ inputs.start_builds }} secrets: inherit From 94faa4cacfb64b6af8eb56d4a0060009cbad78b1 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Tue, 24 Jun 2025 12:38:09 +0200 Subject: [PATCH 071/159] Add read permission to allow for triggering builds --- .github/workflows/find-packages.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/find-packages.yml b/.github/workflows/find-packages.yml index 43c4b6f17e..ad8fb6271d 100644 --- a/.github/workflows/find-packages.yml +++ b/.github/workflows/find-packages.yml @@ -12,7 +12,8 @@ on: required: false type: boolean -permissions: {} +permissions: + contents: read jobs: find-packages: From 7dcc5cd4701d66abc5de9d5e4c8815be11802fbf Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 24 Jun 2025 13:11:00 +0200 Subject: [PATCH 072/159] Expose brush shape to QML CURA-12543 --- plugins/PaintTool/PaintTool.py | 16 +++++++++------- plugins/PaintTool/PaintTool.qml | 5 +++-- plugins/PaintTool/__init__.py | 3 +++ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index e030a789fa..0c3ac0d661 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -3,7 +3,7 @@ from enum import IntEnum import numpy -from PyQt6.QtCore import Qt +from PyQt6.QtCore import Qt, QObject, pyqtEnum from PyQt6.QtGui import QImage, QPainter, QColor, QPen from PyQt6 import QtWidgets from typing import cast, Dict, List, Optional, Tuple @@ -24,9 +24,11 @@ from .PaintView import PaintView class PaintTool(Tool): """Provides the tool to paint meshes.""" - class BrushShape(IntEnum): - SQUARE = 0 - CIRCLE = 1 + class Brush(QObject): + @pyqtEnum + class Shape(IntEnum): + SQUARE = 0 + CIRCLE = 1 def __init__(self) -> None: super().__init__() @@ -41,7 +43,7 @@ class PaintTool(Tool): self._brush_size: int = 10 self._brush_color: str = "A" - self._brush_shape: PaintTool.BrushShape = PaintTool.BrushShape.SQUARE + self._brush_shape: PaintTool.Brush.Shape = PaintTool.Brush.Shape.SQUARE self._brush_pen: QPen = self._createBrushPen() self._mouse_held: bool = False @@ -56,9 +58,9 @@ class PaintTool(Tool): pen.setColor(Qt.GlobalColor.white) match self._brush_shape: - case PaintTool.BrushShape.SQUARE: + case PaintTool.Brush.Shape.SQUARE: pen.setCapStyle(Qt.PenCapStyle.SquareCap) - case PaintTool.BrushShape.CIRCLE: + case PaintTool.Brush.Shape.CIRCLE: pen.setCapStyle(Qt.PenCapStyle.RoundCap) return pen diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml index 8bd02e00a3..602805cba1 100644 --- a/plugins/PaintTool/PaintTool.qml +++ b/plugins/PaintTool/PaintTool.qml @@ -6,6 +6,7 @@ import QtQuick.Controls import QtQuick.Layouts import UM 1.7 as UM +import Cura 1.0 as Cura Item { @@ -154,7 +155,7 @@ Item z: 2 - onClicked: UM.Controller.triggerActionWithData("setBrushShape", 0) + onClicked: UM.Controller.triggerActionWithData("setBrushShape", Cura.PaintToolBrush.SQUARE) } UM.ToolbarButton @@ -171,7 +172,7 @@ Item z: 2 - onClicked: UM.Controller.triggerActionWithData("setBrushShape", 1) + onClicked: UM.Controller.triggerActionWithData("setBrushShape", Cura.PaintToolBrush.CIRCLE) } UM.Slider diff --git a/plugins/PaintTool/__init__.py b/plugins/PaintTool/__init__.py index 301bc49e0d..e92c169ee6 100644 --- a/plugins/PaintTool/__init__.py +++ b/plugins/PaintTool/__init__.py @@ -4,6 +4,8 @@ from . import PaintTool from . import PaintView +from PyQt6.QtQml import qmlRegisterUncreatableType + from UM.i18n import i18nCatalog i18n_catalog = i18nCatalog("cura") @@ -24,6 +26,7 @@ def getMetaData(): } def register(app): + qmlRegisterUncreatableType(PaintTool.PaintTool.Brush, "Cura", 1, 0, "This is an enumeration class", "PaintToolBrush") return { "tool": PaintTool.PaintTool(), "view": PaintView.PaintView() From be14fc7dd63e9c8e3137c0288104a4eae4f081a0 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 24 Jun 2025 13:36:49 +0200 Subject: [PATCH 073/159] Send texture data to the engine CURA-12574 --- cura/Scene/SliceableObjectDecorator.py | 20 +++++++++- plugins/3MFWriter/ThreeMFWriter.py | 43 ++++++++-------------- plugins/CuraEngineBackend/Cura.proto | 2 + plugins/CuraEngineBackend/StartSliceJob.py | 8 ++++ 4 files changed, 45 insertions(+), 28 deletions(-) diff --git a/cura/Scene/SliceableObjectDecorator.py b/cura/Scene/SliceableObjectDecorator.py index dee244b81c..4278705e2e 100644 --- a/cura/Scene/SliceableObjectDecorator.py +++ b/cura/Scene/SliceableObjectDecorator.py @@ -1,8 +1,10 @@ import copy +import json from typing import Optional, Dict -from PyQt6.QtGui import QImage +from PyQt6.QtCore import QBuffer +from PyQt6.QtGui import QImage, QImageWriter import UM.View.GL.Texture from UM.Scene.SceneNodeDecorator import SceneNodeDecorator @@ -40,6 +42,22 @@ class SliceableObjectDecorator(SceneNodeDecorator): def setTextureDataMapping(self, mapping: Dict[str, tuple[int, int]]) -> None: self._texture_data_mapping = mapping + def packTexture(self) -> Optional[bytearray]: + if self._paint_texture is None: + return None + + texture_image = self._paint_texture.getImage() + if texture_image is None: + return None + + texture_buffer = QBuffer() + texture_buffer.open(QBuffer.OpenModeFlag.ReadWrite) + image_writer = QImageWriter(texture_buffer, b"png") + image_writer.setText("Description", json.dumps(self._texture_data_mapping)) + image_writer.write(texture_image) + + return texture_buffer.data() + def __deepcopy__(self, memo) -> "SliceableObjectDecorator": copied_decorator = SliceableObjectDecorator() copied_decorator.setPaintTexture(copy.deepcopy(self.getPaintTexture())) diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index 9d35f88e6d..37345b16b0 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -29,7 +29,7 @@ from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Snapshot import Snapshot from PyQt6.QtCore import Qt, QBuffer -from PyQt6.QtGui import QImage, QPainter, QImageWriter +from PyQt6.QtGui import QImage, QPainter import pySavitar as Savitar from .UCPDialog import UCPDialog @@ -158,36 +158,25 @@ class ThreeMFWriter(MeshWriter): else: savitar_node.getMeshData().setFacesFromBytes(numpy.arange(mesh_data.getVertices().size / 3, dtype=numpy.int32).tobytes()) - texture = um_node.callDecoration("getPaintTexture") + packed_texture = um_node.callDecoration("packTexture") uv_coordinates_array = mesh_data.getUVCoordinatesAsByteArray() - if texture is not None and archive is not None and uv_coordinates_array is not None and len(uv_coordinates_array) > 0: - texture_image = texture.getImage() - if texture_image is not None: - texture_buffer = QBuffer() - texture_buffer.open(QBuffer.OpenModeFlag.ReadWrite) + if packed_texture is not None and archive is not None and uv_coordinates_array is not None and len(uv_coordinates_array) > 0: + texture_path = f"{TEXTURES_PATH}/{id(um_node)}.png" + texture_file = zipfile.ZipInfo(texture_path) + # Don't try to compress texture file, because the PNG is pretty much as compact as it will get + archive.writestr(texture_file, packed_texture) - image_writer = QImageWriter(texture_buffer, b"png") - texture_data_mapping = um_node.callDecoration("getTextureDataMapping") - if texture_data_mapping is not None: - image_writer.setText("Description", json.dumps(texture_data_mapping)) - image_writer.write(texture_image) + savitar_node.getMeshData().setUVCoordinatesPerVertexAsBytes(uv_coordinates_array, texture_path, scene) - texture_path = f"{TEXTURES_PATH}/{id(um_node)}.png" - texture_file = zipfile.ZipInfo(texture_path) - # Don't try to compress texture file, because the PNG is pretty much as compact as it will get - archive.writestr(texture_file, texture_buffer.data()) + # Add texture relation to model relations file + if model_relations_element is not None: + ET.SubElement(model_relations_element, "Relationship", + Target=texture_path, Id=f"rel{len(model_relations_element)+1}", + Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dtexture") - savitar_node.getMeshData().setUVCoordinatesPerVertexAsBytes(uv_coordinates_array, texture_path, scene) - - # Add texture relation to model relations file - if model_relations_element is not None: - ET.SubElement(model_relations_element, "Relationship", - Target=texture_path, Id=f"rel{len(model_relations_element)+1}", - Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dtexture") - - if content_types_element is not None: - ET.SubElement(content_types_element, "Override", PartName=texture_path, - ContentType="application/vnd.ms-package.3dmanufacturing-3dmodeltexture") + if content_types_element is not None: + ET.SubElement(content_types_element, "Override", PartName=texture_path, + ContentType="application/vnd.ms-package.3dmanufacturing-3dmodeltexture") # Handle per object settings (if any) diff --git a/plugins/CuraEngineBackend/Cura.proto b/plugins/CuraEngineBackend/Cura.proto index 8018c9186f..f492718d03 100644 --- a/plugins/CuraEngineBackend/Cura.proto +++ b/plugins/CuraEngineBackend/Cura.proto @@ -53,6 +53,8 @@ message Object bytes indices = 4; //An array of ints. repeated Setting settings = 5; // Setting override per object, overruling the global settings. string name = 6; //Mesh name + bytes uv_coordinates = 7; //An array of 2 floats. + bytes texture = 8; //PNG-encoded texture data } message Progress diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index b276469d09..8b27a0319a 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -509,6 +509,14 @@ class StartSliceJob(Job): obj.vertices = flat_verts + uv_coordinates = mesh_data.getUVCoordinates() + if uv_coordinates is not None: + obj.uv_coordinates = uv_coordinates.flatten() + + packed_texture = object.callDecoration("packTexture") + if packed_texture is not None: + obj.texture = packed_texture + self._handlePerObjectSettings(cast(CuraSceneNode, object), obj) Job.yieldThread() From 6a230b3df5cc57c49550d4b6f262104d44293e54 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Tue, 24 Jun 2025 14:03:07 +0200 Subject: [PATCH 074/159] ClusterPrinterStatus: optional init args Prevents a cura crash if any of the args are not returned from api call --- .../src/Models/Http/ClusterPrinterStatus.py | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py index 925b4844c1..c7250d7c37 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py @@ -20,13 +20,14 @@ from ..BaseModel import BaseModel class ClusterPrinterStatus(BaseModel): """Class representing a cluster printer""" - def __init__(self, enabled: bool, firmware_version: str, friendly_name: str, ip_address: str, machine_variant: str, - status: str, unique_name: str, uuid: str, - configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]], - reserved_by: Optional[str] = None, maintenance_required: Optional[bool] = None, + def __init__(self, enabled: Optional[bool] = True, friendly_name: Optional[str] = "", machine_variant: Optional[str] = "", + status: Optional[str] = "unknown", unique_name: Optional[str] = "", uuid: Optional[str] = "", + configuration: Optional[List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]]] = None, + firmware_version: Optional[str] = None, ip_address: Optional[str] = None, + reserved_by: Optional[str] = None, maintenance_required: Optional[bool] = False, firmware_update_status: Optional[str] = None, latest_available_firmware: Optional[str] = None, - build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None, - material_station: Union[Dict[str, Any], ClusterPrinterMaterialStation] = None, **kwargs) -> None: + build_plate: Optional[Union[Dict[str, Any], ClusterBuildPlate]] = None, + material_station: Optional[Union[Dict[str, Any], ClusterPrinterMaterialStation]] = None, **kwargs) -> None: """ Creates a new cluster printer status :param enabled: A printer can be disabled if it should not receive new jobs. By default, every printer is enabled. @@ -47,7 +48,7 @@ class ClusterPrinterStatus(BaseModel): :param material_station: The material station that is on the printer. """ - self.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration) + self.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration) if configuration else [] self.enabled = enabled self.firmware_version = firmware_version self.friendly_name = friendly_name @@ -70,7 +71,7 @@ class ClusterPrinterStatus(BaseModel): :param controller: - The controller of the model. """ - model = PrinterOutputModel(controller, len(self.configuration), firmware_version = self.firmware_version) + model = PrinterOutputModel(controller, len(self.configuration), firmware_version = self.firmware_version or "") self.updateOutputModel(model) return model @@ -80,13 +81,14 @@ class ClusterPrinterStatus(BaseModel): :param model: - The output model to update. """ - model.updateKey(self.uuid) - model.updateName(self.friendly_name) - model.updateUniqueName(self.unique_name) - model.updateType(self.machine_variant) + model.updateKey(self.uuid or "") + model.updateName(self.friendly_name or "") + model.updateUniqueName(self.unique_name or "") + model.updateType(self.machine_variant or "") model.updateState(self.status if self.enabled else "disabled") model.updateBuildplate(self.build_plate.type if self.build_plate else "glass") - model.setCameraUrl(QUrl("http://{}:8080/?action=stream".format(self.ip_address))) + if self.ip_address: + model.setCameraUrl(QUrl("http://{}:8080/?action=stream".format(self.ip_address))) if not model.printerConfiguration: # Prevent accessing printer configuration when not available. From 161f2707f6ce828396ad0f41c13fed2e46175dcb Mon Sep 17 00:00:00 2001 From: HellAholic Date: Tue, 24 Jun 2025 17:48:30 +0200 Subject: [PATCH 075/159] Update sovol_sv08.def.json - author name - change extruder name (no longer using voron2) --- resources/definitions/sovol_sv08.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/sovol_sv08.def.json b/resources/definitions/sovol_sv08.def.json index 52f5fb6138..e60e6edf91 100644 --- a/resources/definitions/sovol_sv08.def.json +++ b/resources/definitions/sovol_sv08.def.json @@ -5,7 +5,7 @@ "metadata": { "visible": true, - "author": "See voron2_base", + "author": "Steinar H. Gunderson", "manufacturer": "Sovol 3D", "preferred_variant_name": "0.4mm Nozzle", "quality_definition": "sovol_sv08", @@ -17,7 +17,7 @@ "has_machine_quality": true, "has_materials": true, "has_variants": true, - "machine_extruder_trains": { "0": "voron2_extruder_0" }, + "machine_extruder_trains": { "0": "sovol_sv08_extruder" }, "preferred_material": "generic_abs", "preferred_quality_type": "fast" }, From 2cfb063c9b3c03378ad47fd0f6f088605807ad10 Mon Sep 17 00:00:00 2001 From: HellAholic <28710690+HellAholic@users.noreply.github.com> Date: Tue, 24 Jun 2025 15:49:28 +0000 Subject: [PATCH 076/159] Apply printer-linter format --- resources/definitions/sovol_sv08.def.json | 52 +++++++++++------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/resources/definitions/sovol_sv08.def.json b/resources/definitions/sovol_sv08.def.json index e60e6edf91..a0cda31889 100644 --- a/resources/definitions/sovol_sv08.def.json +++ b/resources/definitions/sovol_sv08.def.json @@ -7,11 +7,8 @@ "visible": true, "author": "Steinar H. Gunderson", "manufacturer": "Sovol 3D", - "preferred_variant_name": "0.4mm Nozzle", - "quality_definition": "sovol_sv08", - "variants_name": "Nozzle Size", - "platform": "sovol_sv08_buildplate_model.stl", "file_formats": "text/x-gcode", + "platform": "sovol_sv08_buildplate_model.stl", "exclude_materials": [], "first_start_actions": [ "MachineSettingsAction" ], "has_machine_quality": true, @@ -19,30 +16,13 @@ "has_variants": true, "machine_extruder_trains": { "0": "sovol_sv08_extruder" }, "preferred_material": "generic_abs", - "preferred_quality_type": "fast" + "preferred_quality_type": "fast", + "preferred_variant_name": "0.4mm Nozzle", + "quality_definition": "sovol_sv08", + "variants_name": "Nozzle Size" }, "overrides": { - "machine_depth": { "default_value": 350 }, - "machine_width": { "default_value": 350 }, - "machine_height": { "default_value": 345 }, - "machine_name": { "default_value": "SV08" }, - "retraction_amount": { "default_value": 0.5 }, - "machine_max_acceleration_x": { "default_value": 40000 }, - "machine_max_acceleration_y": { "default_value": 40000 }, - "machine_max_acceleration_z": { "default_value": 500 }, - "machine_max_acceleration_e": { "default_value": 5000 }, - "machine_max_feedrate_x": { "default_value": 700 }, - "machine_max_feedrate_y": { "default_value": 700 }, - "machine_max_feedrate_z": { "default_value": 20 }, - "machine_max_feedrate_e": { "default_value": 50 }, - "machine_max_jerk_e": { "default_value": 5 }, - "machine_max_jerk_xy": { "default_value": 20 }, - "machine_max_jerk_z": { "default_value": 0.5 }, - "retraction_min_travel": { "default_value": 1 }, - "retraction_hop": { "default_value": 0.4 }, - "machine_start_gcode": { "default_value": "G28 ; Move to zero\nG90 ; Absolute positioning\nG1 X0 F9000\nG1 Y20\nG1 Z0.600 F600\nG1 Y0 F9000\nSTART_PRINT EXTRUDER_TEMP={material_print_temperature_layer_0} BED_TEMP={material_bed_temperature_layer_0}\nG90 ; Absolute positioning (START_PRINT might have changed it)\nG1 X0 F9000\nG1 Y20\nG1 Z0.600 F600\nG1 Y0 F9000\nM400\nG91 ; Relative positioning\nM83 ; Relative extrusion\nM140 S{material_bed_temperature_layer_0} ; Set bed temp\nM104 S{material_print_temperature_layer_0} ; Set extruder temp\nM190 S{material_bed_temperature_layer_0} ; Wait for bed temp\nM109 S{material_print_temperature_layer_0} ; Wait for extruder temp\n{if machine_nozzle_size >= 0.4}\n; Standard Sovol blob and purge line.\nG1 E25 F300 ; Purge blob\nG4 P1000 ; Wait a bit to let it finish\nG1 E-0.200 Z5 F600 ; Retract\nG1 X88.000 F9000 ; Move right and then down for purge line\nG1 Z-5.000 F600\nG1 X87.000 E20.88 F1800 ; Purge line right\nG1 X87.000 E13.92 F1800\nG1 Y1 E0.16 F1800 ; Small movement back for next line\nG1 X-87.000 E13.92 F1800 ; Purge line left\nG1 X-87.000 E20.88 F1800\nG1 Y1 E0.24 F1800 ; Small movement back for next line\nG1 X87.000 E20.88 F1800 ; Purge line right\nG1 X87.000 E13.92 F1800\nG1 E-0.200 Z1 F600\n{else}\n; The default start G-code uses too high flow for smaller nozzles,\n; which causes Klipper errors. Scale everything back by\n; (0.25/0.4)^2, i.e., for 0.25mm nozzle. This should be good\n; enough for 0.2mm as well.\nG1 E8 F300 ; Purge blob\nG4 P1000 ; Wait a bit to let it finish\nG1 E-0.078 Z5 F600 ; Retract\nG1 X88.000 F9000 ; Move right and then down for purge line\nG1 Z-5.000 F600\nG1 X87.000 E8.16 F1800 ; Purge line right\nG1 X87.000 E5.44 F1800\nG1 Y1 E0.063 F1800 ; Small movement back for next line\nG1 X-87.000 E5.44 F1800 ; Purge line left\nG1 X-87.000 E8.16 F1800\nG1 Y1 E0.094 F1800 ; Small movement back for next line\nG1 X87.000 E8.16 F1800 ; Purge line right\nG1 X87.000 E5.44 F1800\nG1 E-0.078 Z1 F600\n{endif}\nM400 ; Wait for moves to finish\nG90 ; Absolute positioning\nM82 ; Absolute extrusion mode\n" }, - "machine_end_gcode": { "default_value": "END_PRINT\n" }, "acceleration_enabled": { "default_value": false }, "acceleration_layer_0": { "value": 1800 }, "acceleration_print": { "default_value": 2200 }, @@ -71,6 +51,8 @@ "layer_height_0": { "resolve": "max(0.2, min(extruderValues('layer_height')))" }, "line_width": { "value": "machine_nozzle_size * 1.125" }, "machine_acceleration": { "default_value": 1500 }, + "machine_depth": { "default_value": 350 }, + "machine_end_gcode": { "default_value": "END_PRINT\n" }, "machine_endstop_positive_direction_x": { "default_value": true }, "machine_endstop_positive_direction_y": { "default_value": true }, "machine_endstop_positive_direction_z": { "default_value": false }, @@ -86,16 +68,34 @@ ] }, "machine_heated_bed": { "default_value": true }, + "machine_height": { "default_value": 345 }, + "machine_max_acceleration_e": { "default_value": 5000 }, + "machine_max_acceleration_x": { "default_value": 40000 }, + "machine_max_acceleration_y": { "default_value": 40000 }, + "machine_max_acceleration_z": { "default_value": 500 }, + "machine_max_feedrate_e": { "default_value": 50 }, + "machine_max_feedrate_x": { "default_value": 700 }, + "machine_max_feedrate_y": { "default_value": 700 }, + "machine_max_feedrate_z": { "default_value": 20 }, + "machine_max_jerk_e": { "default_value": 5 }, + "machine_max_jerk_xy": { "default_value": 20 }, + "machine_max_jerk_z": { "default_value": 0.5 }, + "machine_name": { "default_value": "SV08" }, + "machine_start_gcode": { "default_value": "G28 ; Move to zero\nG90 ; Absolute positioning\nG1 X0 F9000\nG1 Y20\nG1 Z0.600 F600\nG1 Y0 F9000\nSTART_PRINT EXTRUDER_TEMP={material_print_temperature_layer_0} BED_TEMP={material_bed_temperature_layer_0}\nG90 ; Absolute positioning (START_PRINT might have changed it)\nG1 X0 F9000\nG1 Y20\nG1 Z0.600 F600\nG1 Y0 F9000\nM400\nG91 ; Relative positioning\nM83 ; Relative extrusion\nM140 S{material_bed_temperature_layer_0} ; Set bed temp\nM104 S{material_print_temperature_layer_0} ; Set extruder temp\nM190 S{material_bed_temperature_layer_0} ; Wait for bed temp\nM109 S{material_print_temperature_layer_0} ; Wait for extruder temp\n{if machine_nozzle_size >= 0.4}\n; Standard Sovol blob and purge line.\nG1 E25 F300 ; Purge blob\nG4 P1000 ; Wait a bit to let it finish\nG1 E-0.200 Z5 F600 ; Retract\nG1 X88.000 F9000 ; Move right and then down for purge line\nG1 Z-5.000 F600\nG1 X87.000 E20.88 F1800 ; Purge line right\nG1 X87.000 E13.92 F1800\nG1 Y1 E0.16 F1800 ; Small movement back for next line\nG1 X-87.000 E13.92 F1800 ; Purge line left\nG1 X-87.000 E20.88 F1800\nG1 Y1 E0.24 F1800 ; Small movement back for next line\nG1 X87.000 E20.88 F1800 ; Purge line right\nG1 X87.000 E13.92 F1800\nG1 E-0.200 Z1 F600\n{else}\n; The default start G-code uses too high flow for smaller nozzles,\n; which causes Klipper errors. Scale everything back by\n; (0.25/0.4)^2, i.e., for 0.25mm nozzle. This should be good\n; enough for 0.2mm as well.\nG1 E8 F300 ; Purge blob\nG4 P1000 ; Wait a bit to let it finish\nG1 E-0.078 Z5 F600 ; Retract\nG1 X88.000 F9000 ; Move right and then down for purge line\nG1 Z-5.000 F600\nG1 X87.000 E8.16 F1800 ; Purge line right\nG1 X87.000 E5.44 F1800\nG1 Y1 E0.063 F1800 ; Small movement back for next line\nG1 X-87.000 E5.44 F1800 ; Purge line left\nG1 X-87.000 E8.16 F1800\nG1 Y1 E0.094 F1800 ; Small movement back for next line\nG1 X87.000 E8.16 F1800 ; Purge line right\nG1 X87.000 E5.44 F1800\nG1 E-0.078 Z1 F600\n{endif}\nM400 ; Wait for moves to finish\nG90 ; Absolute positioning\nM82 ; Absolute extrusion mode\n" }, "machine_steps_per_mm_x": { "default_value": 80 }, "machine_steps_per_mm_y": { "default_value": 80 }, "machine_steps_per_mm_z": { "default_value": 400 }, + "machine_width": { "default_value": 350 }, "meshfix_maximum_resolution": { "default_value": 0.01 }, "min_infill_area": { "default_value": 5.0 }, "minimum_polygon_circumference": { "default_value": 0.2 }, "optimize_wall_printing_order": { "default_value": true }, + "retraction_amount": { "default_value": 0.5 }, "retraction_combing": { "value": "'noskin'" }, "retraction_combing_max_distance": { "default_value": 10 }, + "retraction_hop": { "default_value": 0.4 }, "retraction_hop_enabled": { "default_value": true }, + "retraction_min_travel": { "default_value": 1 }, "retraction_prime_speed": { "maximum_value_warning": 130, @@ -128,4 +128,4 @@ "wall_overhang_speed_factor": { "default_value": 50 }, "zig_zaggify_infill": { "value": true } } -} +} \ No newline at end of file From 594ad121ddd0c1218fbbe88ab440e264deb95587 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Tue, 24 Jun 2025 17:49:44 +0200 Subject: [PATCH 077/159] bump setting version to 25 --- resources/variants/sovol/sovol_sv08_0.4.inst.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/variants/sovol/sovol_sv08_0.4.inst.cfg b/resources/variants/sovol/sovol_sv08_0.4.inst.cfg index ce8ae1f23e..b5c72e92d3 100644 --- a/resources/variants/sovol/sovol_sv08_0.4.inst.cfg +++ b/resources/variants/sovol/sovol_sv08_0.4.inst.cfg @@ -5,7 +5,7 @@ version = 4 [metadata] hardware_type = nozzle -setting_version = 23 +setting_version = 25 type = variant [values] From 08d14c39b4ad7bd70ec336b65acb2fd65bb15146 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Tue, 24 Jun 2025 17:50:07 +0200 Subject: [PATCH 078/159] bump setting version to 25 --- resources/quality/sovol/sovol_sv08_global.inst.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/quality/sovol/sovol_sv08_global.inst.cfg b/resources/quality/sovol/sovol_sv08_global.inst.cfg index 2f2f0004ab..bebbc3acb1 100644 --- a/resources/quality/sovol/sovol_sv08_global.inst.cfg +++ b/resources/quality/sovol/sovol_sv08_global.inst.cfg @@ -6,7 +6,7 @@ version = 4 [metadata] global_quality = True quality_type = standard -setting_version = 23 +setting_version = 25 type = quality [values] From 75a719ec6031e7300848f10c672cacef99626fde Mon Sep 17 00:00:00 2001 From: HellAholic <28710690+HellAholic@users.noreply.github.com> Date: Tue, 24 Jun 2025 15:51:48 +0000 Subject: [PATCH 079/159] Apply printer-linter format --- .../quality/sovol/sovol_sv08_global.inst.cfg | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/resources/quality/sovol/sovol_sv08_global.inst.cfg b/resources/quality/sovol/sovol_sv08_global.inst.cfg index bebbc3acb1..4030ec9441 100644 --- a/resources/quality/sovol/sovol_sv08_global.inst.cfg +++ b/resources/quality/sovol/sovol_sv08_global.inst.cfg @@ -10,21 +10,22 @@ setting_version = 25 type = quality [values] -layer_height = 0.2 -speed_print = 600 -speed_wall_x = 300 -speed_wall_0 = 200 -speed_infill = 200 -speed_travel = =speed_print -skirt_brim_speed = 80 -speed_ironing = 15 -speed_layer_0 = 30 -speed_slowdown_layers = 3 acceleration_enabled = True +acceleration_layer_0 = 3000 acceleration_print = 20000 -acceleration_wall_0 = 8000 -acceleration_wall_x = 12000 acceleration_roofing = =acceleration_wall_0 acceleration_topbottom = =acceleration_wall -acceleration_layer_0 = 3000 acceleration_travel = 40000 +acceleration_wall_0 = 8000 +acceleration_wall_x = 12000 +layer_height = 0.2 +skirt_brim_speed = 80 +speed_infill = 200 +speed_ironing = 15 +speed_layer_0 = 30 +speed_print = 600 +speed_slowdown_layers = 3 +speed_travel = =speed_print +speed_wall_0 = 200 +speed_wall_x = 300 + From 9cf75648ab527579f2e0c1e5a051b56581c2c5e9 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Wed, 25 Jun 2025 13:54:56 +0200 Subject: [PATCH 080/159] Review comment - Set default to `""` for `Optional[str]` to remove the `or ""` -The FW version can be returned as None which requires the fallback `or ""` --- .../src/Models/Http/ClusterPrinterStatus.py | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py index c7250d7c37..260d276427 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py @@ -20,14 +20,23 @@ from ..BaseModel import BaseModel class ClusterPrinterStatus(BaseModel): """Class representing a cluster printer""" - def __init__(self, enabled: Optional[bool] = True, friendly_name: Optional[str] = "", machine_variant: Optional[str] = "", - status: Optional[str] = "unknown", unique_name: Optional[str] = "", uuid: Optional[str] = "", + def __init__(self, + enabled: Optional[bool] = True, + friendly_name: Optional[str] = "", + machine_variant: Optional[str] = "", + status: Optional[str] = "unknown", + unique_name: Optional[str] = "", + uuid: Optional[str] = "", configuration: Optional[List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]]] = None, - firmware_version: Optional[str] = None, ip_address: Optional[str] = None, - reserved_by: Optional[str] = None, maintenance_required: Optional[bool] = False, - firmware_update_status: Optional[str] = None, latest_available_firmware: Optional[str] = None, + firmware_version: Optional[str] = None, + ip_address: Optional[str] = None, + reserved_by: Optional[str] = "", + maintenance_required: Optional[bool] = False, + firmware_update_status: Optional[str] = "", + latest_available_firmware: Optional[str] = "", build_plate: Optional[Union[Dict[str, Any], ClusterBuildPlate]] = None, - material_station: Optional[Union[Dict[str, Any], ClusterPrinterMaterialStation]] = None, **kwargs) -> None: + material_station: Optional[Union[Dict[str, Any], ClusterPrinterMaterialStation]] = None, + **kwargs) -> None: """ Creates a new cluster printer status :param enabled: A printer can be disabled if it should not receive new jobs. By default, every printer is enabled. @@ -81,10 +90,10 @@ class ClusterPrinterStatus(BaseModel): :param model: - The output model to update. """ - model.updateKey(self.uuid or "") - model.updateName(self.friendly_name or "") - model.updateUniqueName(self.unique_name or "") - model.updateType(self.machine_variant or "") + model.updateKey(self.uuid) + model.updateName(self.friendly_name) + model.updateUniqueName(self.unique_name) + model.updateType(self.machine_variant) model.updateState(self.status if self.enabled else "disabled") model.updateBuildplate(self.build_plate.type if self.build_plate else "glass") if self.ip_address: From 90170c694ccb09ec4e7d62ad5ebbe6ca735805b5 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Thu, 26 Jun 2025 09:23:20 +0200 Subject: [PATCH 081/159] Unlink the reusable workflows and add build args --- .github/workflows/find-packages.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/find-packages.yml b/.github/workflows/find-packages.yml index ad8fb6271d..671121904a 100644 --- a/.github/workflows/find-packages.yml +++ b/.github/workflows/find-packages.yml @@ -11,6 +11,18 @@ on: default: false required: false type: boolean + conan_args: + description: 'Conan args' + default: '' + type: string + enterprise: + description: 'Build Cura as an Enterprise edition' + default: false + type: boolean + staging: + description: 'Use staging API' + default: false + type: boolean permissions: contents: read @@ -23,3 +35,17 @@ jobs: jira_ticket_number: ${{ inputs.jira_ticket_number }} start_builds: ${{ inputs.start_builds }} secrets: inherit + + installers: + name: Create installers + needs: find-packages + if: ${{ inputs.start_builds == true && needs.find-packages.outputs.cura_conan_version != '' }} + uses: ultimaker/cura-workflows/.github/workflows/cura-installers.yml@main + with: + cura_conan_version: ${{ needs.find-packages.outputs.cura_conan_version }} + package_overrides: ${{ needs.find-packages.outputs.package_overrides }} + conan_args: ${{ inputs.conan_args }} + enterprise: ${{ inputs.enterprise }} + staging: ${{ inputs.staging }} + secrets: inherit + From 06cf1151e5e7e63285b11de8c3ddedb28c334ddd Mon Sep 17 00:00:00 2001 From: HellAholic Date: Thu, 26 Jun 2025 09:26:46 +0200 Subject: [PATCH 082/159] remove input for starting build from workflow - installers job is moved to this workflow so the trigger to start build is handled locally and should not be sent to reusable workflow --- .github/workflows/find-packages.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/find-packages.yml b/.github/workflows/find-packages.yml index 671121904a..60f87c8688 100644 --- a/.github/workflows/find-packages.yml +++ b/.github/workflows/find-packages.yml @@ -33,7 +33,6 @@ jobs: uses: ultimaker/cura-workflows/.github/workflows/find-package-by-ticket.yml@jira_find_package with: jira_ticket_number: ${{ inputs.jira_ticket_number }} - start_builds: ${{ inputs.start_builds }} secrets: inherit installers: From 9cc61d7db092427cb077aa8371a5e0f00970ba85 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Thu, 26 Jun 2025 09:44:01 +0200 Subject: [PATCH 083/159] workflow input/output name correction --- .github/workflows/find-packages.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/find-packages.yml b/.github/workflows/find-packages.yml index 60f87c8688..fa2dfe2d39 100644 --- a/.github/workflows/find-packages.yml +++ b/.github/workflows/find-packages.yml @@ -38,10 +38,10 @@ jobs: installers: name: Create installers needs: find-packages - if: ${{ inputs.start_builds == true && needs.find-packages.outputs.cura_conan_version != '' }} + if: ${{ inputs.start_builds == true && needs.find-packages.outputs.discovered_packages != '' }} uses: ultimaker/cura-workflows/.github/workflows/cura-installers.yml@main with: - cura_conan_version: ${{ needs.find-packages.outputs.cura_conan_version }} + cura_conan_version: ${{ needs.find-packages.outputs.cura_package }} package_overrides: ${{ needs.find-packages.outputs.package_overrides }} conan_args: ${{ inputs.conan_args }} enterprise: ${{ inputs.enterprise }} From 300d776f1a1a96e3d278ffd5db0db07ee77ede89 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Thu, 26 Jun 2025 09:48:46 +0200 Subject: [PATCH 084/159] adjust to access the workflow_dispatch input inputs.start_builds is only available at the top-level workflow, not inside a job's if condition. --- .github/workflows/find-packages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/find-packages.yml b/.github/workflows/find-packages.yml index fa2dfe2d39..4e584844d6 100644 --- a/.github/workflows/find-packages.yml +++ b/.github/workflows/find-packages.yml @@ -38,7 +38,7 @@ jobs: installers: name: Create installers needs: find-packages - if: ${{ inputs.start_builds == true && needs.find-packages.outputs.discovered_packages != '' }} + if: ${{ github.event.inputs.start_builds == 'true' && needs.find-packages.outputs.discovered_packages != '' }} uses: ultimaker/cura-workflows/.github/workflows/cura-installers.yml@main with: cura_conan_version: ${{ needs.find-packages.outputs.cura_package }} From a6bb69566665c99e210e2d7045110a3dcc699f69 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Thu, 26 Jun 2025 10:04:28 +0200 Subject: [PATCH 085/159] debug - remove the input check --- .github/workflows/find-packages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/find-packages.yml b/.github/workflows/find-packages.yml index 4e584844d6..283e59286e 100644 --- a/.github/workflows/find-packages.yml +++ b/.github/workflows/find-packages.yml @@ -38,7 +38,7 @@ jobs: installers: name: Create installers needs: find-packages - if: ${{ github.event.inputs.start_builds == 'true' && needs.find-packages.outputs.discovered_packages != '' }} + if: ${{ needs.find-packages.outputs.discovered_packages != '' }} uses: ultimaker/cura-workflows/.github/workflows/cura-installers.yml@main with: cura_conan_version: ${{ needs.find-packages.outputs.cura_package }} From 7637bbe3ca8abeccf4fd7d5842712e743e9dd6db Mon Sep 17 00:00:00 2001 From: HellAholic Date: Thu, 26 Jun 2025 10:07:03 +0200 Subject: [PATCH 086/159] debug output --- .github/workflows/find-packages.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/find-packages.yml b/.github/workflows/find-packages.yml index 283e59286e..d9772dd782 100644 --- a/.github/workflows/find-packages.yml +++ b/.github/workflows/find-packages.yml @@ -35,6 +35,16 @@ jobs: jira_ticket_number: ${{ inputs.jira_ticket_number }} secrets: inherit + debug-outputs: + name: Debug Outputs + needs: find-packages + runs-on: ubuntu-latest + steps: + - run: | + echo "discovered_packages: '${{ needs.find-packages.outputs.discovered_packages }}'" + echo "cura_package: '${{ needs.find-packages.outputs.cura_package }}'" + echo "package_overrides: '${{ needs.find-packages.outputs.package_overrides }}'" + installers: name: Create installers needs: find-packages From f2c754ef7a3c3f41c73279724bf7a09ebea9e1fa Mon Sep 17 00:00:00 2001 From: HellAholic Date: Thu, 26 Jun 2025 10:16:13 +0200 Subject: [PATCH 087/159] Remove debug and set the start_builds condition back --- .github/workflows/find-packages.yml | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/.github/workflows/find-packages.yml b/.github/workflows/find-packages.yml index d9772dd782..fa2dfe2d39 100644 --- a/.github/workflows/find-packages.yml +++ b/.github/workflows/find-packages.yml @@ -35,20 +35,10 @@ jobs: jira_ticket_number: ${{ inputs.jira_ticket_number }} secrets: inherit - debug-outputs: - name: Debug Outputs - needs: find-packages - runs-on: ubuntu-latest - steps: - - run: | - echo "discovered_packages: '${{ needs.find-packages.outputs.discovered_packages }}'" - echo "cura_package: '${{ needs.find-packages.outputs.cura_package }}'" - echo "package_overrides: '${{ needs.find-packages.outputs.package_overrides }}'" - installers: name: Create installers needs: find-packages - if: ${{ needs.find-packages.outputs.discovered_packages != '' }} + if: ${{ inputs.start_builds == true && needs.find-packages.outputs.discovered_packages != '' }} uses: ultimaker/cura-workflows/.github/workflows/cura-installers.yml@main with: cura_conan_version: ${{ needs.find-packages.outputs.cura_package }} From f506fe570949559a5da33c49d43c264626b98322 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Thu, 26 Jun 2025 10:22:22 +0200 Subject: [PATCH 088/159] Update find-packages.yml --- .github/workflows/find-packages.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/find-packages.yml b/.github/workflows/find-packages.yml index fa2dfe2d39..81f68857e5 100644 --- a/.github/workflows/find-packages.yml +++ b/.github/workflows/find-packages.yml @@ -1,4 +1,4 @@ -name: Conan Package Discovery by Jira Ticket +name: Find packages for Jira ticket and create installers on: workflow_dispatch: @@ -30,7 +30,7 @@ permissions: jobs: find-packages: name: Find packages for Jira ticket - uses: ultimaker/cura-workflows/.github/workflows/find-package-by-ticket.yml@jira_find_package + uses: ultimaker/cura-workflows/.github/workflows/find-package-by-ticket.yml@main with: jira_ticket_number: ${{ inputs.jira_ticket_number }} secrets: inherit @@ -47,4 +47,3 @@ jobs: enterprise: ${{ inputs.enterprise }} staging: ${{ inputs.staging }} secrets: inherit - From 21764d3db00a573aa3635fe1635d995ec85b034d Mon Sep 17 00:00:00 2001 From: HellAholic Date: Thu, 26 Jun 2025 10:38:48 +0200 Subject: [PATCH 089/159] Remove redundant override The value is overwritten with the formula in fdmprinter, so setting the default_value does not have any effect in this case. --- resources/definitions/sovol_sv08.def.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/definitions/sovol_sv08.def.json b/resources/definitions/sovol_sv08.def.json index a0cda31889..e64cca8a06 100644 --- a/resources/definitions/sovol_sv08.def.json +++ b/resources/definitions/sovol_sv08.def.json @@ -95,7 +95,6 @@ "retraction_combing_max_distance": { "default_value": 10 }, "retraction_hop": { "default_value": 0.4 }, "retraction_hop_enabled": { "default_value": true }, - "retraction_min_travel": { "default_value": 1 }, "retraction_prime_speed": { "maximum_value_warning": 130, @@ -128,4 +127,4 @@ "wall_overhang_speed_factor": { "default_value": 50 }, "zig_zaggify_infill": { "value": true } } -} \ No newline at end of file +} From 445ea884ffc5837610801a32dbf73ab7da434f69 Mon Sep 17 00:00:00 2001 From: HellAholic <28710690+HellAholic@users.noreply.github.com> Date: Thu, 26 Jun 2025 08:39:47 +0000 Subject: [PATCH 090/159] Apply printer-linter format --- resources/definitions/sovol_sv08.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/sovol_sv08.def.json b/resources/definitions/sovol_sv08.def.json index e64cca8a06..a0b7125a9e 100644 --- a/resources/definitions/sovol_sv08.def.json +++ b/resources/definitions/sovol_sv08.def.json @@ -127,4 +127,4 @@ "wall_overhang_speed_factor": { "default_value": 50 }, "zig_zaggify_infill": { "value": true } } -} +} \ No newline at end of file From 8bfd3047548c41432fa374f7dcc4c1f87258704e Mon Sep 17 00:00:00 2001 From: HellAholic Date: Thu, 26 Jun 2025 14:26:51 +0200 Subject: [PATCH 091/159] definition fix wall_overhang_speed_factor: Int should be wall_overhang_speed_factors: List --- resources/definitions/sovol_sv08.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/sovol_sv08.def.json b/resources/definitions/sovol_sv08.def.json index a0b7125a9e..c3896304fe 100644 --- a/resources/definitions/sovol_sv08.def.json +++ b/resources/definitions/sovol_sv08.def.json @@ -124,7 +124,7 @@ "travel_avoid_other_parts": { "default_value": false }, "wall_line_width": { "value": "machine_nozzle_size" }, "wall_overhang_angle": { "default_value": 75 }, - "wall_overhang_speed_factor": { "default_value": 50 }, + "wall_overhang_speed_factors": { "default_value": [50] }, "zig_zaggify_infill": { "value": true } } } \ No newline at end of file From ccea5bcfee63e8ccea0615f7e02f7ad7bf035949 Mon Sep 17 00:00:00 2001 From: HellAholic <28710690+HellAholic@users.noreply.github.com> Date: Thu, 26 Jun 2025 12:27:49 +0000 Subject: [PATCH 092/159] Apply printer-linter format --- resources/definitions/sovol_sv08.def.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/resources/definitions/sovol_sv08.def.json b/resources/definitions/sovol_sv08.def.json index c3896304fe..95a03f6e99 100644 --- a/resources/definitions/sovol_sv08.def.json +++ b/resources/definitions/sovol_sv08.def.json @@ -124,7 +124,12 @@ "travel_avoid_other_parts": { "default_value": false }, "wall_line_width": { "value": "machine_nozzle_size" }, "wall_overhang_angle": { "default_value": 75 }, - "wall_overhang_speed_factors": { "default_value": [50] }, + "wall_overhang_speed_factors": + { + "default_value": [ + 50 + ] + }, "zig_zaggify_infill": { "value": true } } } \ No newline at end of file From dd12c8152c14dde1495ea7012baabcf4e8a89a04 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Thu, 26 Jun 2025 14:47:21 +0200 Subject: [PATCH 093/159] overhang speed factors update to string(list) --- resources/definitions/sovol_sv08.def.json | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/resources/definitions/sovol_sv08.def.json b/resources/definitions/sovol_sv08.def.json index 95a03f6e99..147b839fbf 100644 --- a/resources/definitions/sovol_sv08.def.json +++ b/resources/definitions/sovol_sv08.def.json @@ -124,12 +124,7 @@ "travel_avoid_other_parts": { "default_value": false }, "wall_line_width": { "value": "machine_nozzle_size" }, "wall_overhang_angle": { "default_value": 75 }, - "wall_overhang_speed_factors": - { - "default_value": [ - 50 - ] - }, + "wall_overhang_speed_factors": { "default_value": "[50]" }, "zig_zaggify_infill": { "value": true } } -} \ No newline at end of file +} From b73c5091ac6f8cdcb27f255473218d50c2d047bd Mon Sep 17 00:00:00 2001 From: HellAholic <28710690+HellAholic@users.noreply.github.com> Date: Thu, 26 Jun 2025 12:48:18 +0000 Subject: [PATCH 094/159] Apply printer-linter format --- resources/definitions/sovol_sv08.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/sovol_sv08.def.json b/resources/definitions/sovol_sv08.def.json index 147b839fbf..a662b19618 100644 --- a/resources/definitions/sovol_sv08.def.json +++ b/resources/definitions/sovol_sv08.def.json @@ -127,4 +127,4 @@ "wall_overhang_speed_factors": { "default_value": "[50]" }, "zig_zaggify_infill": { "value": true } } -} +} \ No newline at end of file From 39c791ba470d42d56c221cebf71d4fd07928cbb7 Mon Sep 17 00:00:00 2001 From: HellAholic <28710690+HellAholic@users.noreply.github.com> Date: Thu, 26 Jun 2025 15:01:17 +0000 Subject: [PATCH 095/159] Apply printer-linter format --- resources/extruders/sovol_sv08_extruder.def.json | 2 +- .../sovol/ABS/sovol_sv08_0.4_ABS_standard.inst.cfg | 9 +++++---- .../sovol/PETG/sovol_sv08_0.4_PETG_standard.inst.cfg | 9 +++++---- .../sovol/PLA/sovol_sv08_0.4_PLA_standard.inst.cfg | 9 +++++---- .../sovol/TPU/sovol_sv08_0.4_TPU_standard.inst.cfg | 9 +++++---- 5 files changed, 21 insertions(+), 17 deletions(-) diff --git a/resources/extruders/sovol_sv08_extruder.def.json b/resources/extruders/sovol_sv08_extruder.def.json index 407c81de47..d0ccdee1de 100644 --- a/resources/extruders/sovol_sv08_extruder.def.json +++ b/resources/extruders/sovol_sv08_extruder.def.json @@ -16,4 +16,4 @@ }, "material_diameter": { "default_value": 1.75 } } -} +} \ No newline at end of file diff --git a/resources/quality/sovol/ABS/sovol_sv08_0.4_ABS_standard.inst.cfg b/resources/quality/sovol/ABS/sovol_sv08_0.4_ABS_standard.inst.cfg index 2fda8d1811..4fdd35f5ba 100644 --- a/resources/quality/sovol/ABS/sovol_sv08_0.4_ABS_standard.inst.cfg +++ b/resources/quality/sovol/ABS/sovol_sv08_0.4_ABS_standard.inst.cfg @@ -11,16 +11,17 @@ type = quality variant = 0.4mm Nozzle [values] +bridge_fan_speed = 30 +bridge_settings_enabled = True cool_fan_enabled = True cool_fan_speed = 10 cool_fan_speed_max = 30 -cool_min_layer_time_fan_speed_max = 30 cool_min_layer_time = 4 +cool_min_layer_time_fan_speed_max = 30 cool_min_speed = 10 +material_bed_temperature = 95 material_flow = 98 material_max_flowrate = 21 material_print_temperature = 270 material_print_temperature_layer_0 = 280 -material_bed_temperature = 95 -bridge_settings_enabled = True -bridge_fan_speed = 30 + diff --git a/resources/quality/sovol/PETG/sovol_sv08_0.4_PETG_standard.inst.cfg b/resources/quality/sovol/PETG/sovol_sv08_0.4_PETG_standard.inst.cfg index ba43c72c8b..3cf9d68d04 100644 --- a/resources/quality/sovol/PETG/sovol_sv08_0.4_PETG_standard.inst.cfg +++ b/resources/quality/sovol/PETG/sovol_sv08_0.4_PETG_standard.inst.cfg @@ -11,16 +11,17 @@ type = quality variant = 0.4mm Nozzle [values] +bridge_fan_speed = 70 +bridge_settings_enabled = True cool_fan_enabled = True cool_fan_speed = 10 cool_fan_speed_max = 30 -cool_min_layer_time_fan_speed_max = 30 cool_min_layer_time = 5 +cool_min_layer_time_fan_speed_max = 30 cool_min_speed = 10 +material_bed_temperature = 75 material_flow = 98 material_max_flowrate = 17 material_print_temperature = 235 material_print_temperature_layer_0 = 250 -material_bed_temperature = 75 -bridge_settings_enabled = True -bridge_fan_speed = 70 + diff --git a/resources/quality/sovol/PLA/sovol_sv08_0.4_PLA_standard.inst.cfg b/resources/quality/sovol/PLA/sovol_sv08_0.4_PLA_standard.inst.cfg index 2a70b9415b..daca9ebd94 100644 --- a/resources/quality/sovol/PLA/sovol_sv08_0.4_PLA_standard.inst.cfg +++ b/resources/quality/sovol/PLA/sovol_sv08_0.4_PLA_standard.inst.cfg @@ -11,16 +11,17 @@ type = quality variant = 0.4mm Nozzle [values] +bridge_fan_speed = 100 +bridge_settings_enabled = True cool_fan_enabled = True cool_fan_speed = 50 cool_fan_speed_max = 70 -cool_min_layer_time_fan_speed_max = 50 cool_min_layer_time = 5 +cool_min_layer_time_fan_speed_max = 50 cool_min_speed = 10 +material_bed_temperature = 65 material_flow = 98 material_max_flowrate = 21 material_print_temperature = 220 material_print_temperature_layer_0 = 235 -material_bed_temperature = 65 -bridge_settings_enabled = True -bridge_fan_speed = 100 + diff --git a/resources/quality/sovol/TPU/sovol_sv08_0.4_TPU_standard.inst.cfg b/resources/quality/sovol/TPU/sovol_sv08_0.4_TPU_standard.inst.cfg index 1908bdb6b0..c1b44b46b0 100644 --- a/resources/quality/sovol/TPU/sovol_sv08_0.4_TPU_standard.inst.cfg +++ b/resources/quality/sovol/TPU/sovol_sv08_0.4_TPU_standard.inst.cfg @@ -11,16 +11,17 @@ type = quality variant = 0.4mm Nozzle [values] +bridge_fan_speed = 100 +bridge_settings_enabled = True cool_fan_enabled = True cool_fan_speed = 80 cool_fan_speed_max = 100 -cool_min_layer_time_fan_speed_max = 50 cool_min_layer_time = 5 +cool_min_layer_time_fan_speed_max = 50 cool_min_speed = 10 +material_bed_temperature = 65 material_flow = 98 material_max_flowrate = 3.6 material_print_temperature = 240 material_print_temperature_layer_0 = 235 -material_bed_temperature = 65 -bridge_settings_enabled = True -bridge_fan_speed = 100 + From ae2a189c14dbab873b197b9235e8382847a7cf7e Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 30 Jun 2025 09:53:54 +0200 Subject: [PATCH 096/159] Replace "cloudActive" property by generic "active" CURA-12557 --- .../NetworkedPrinterOutputDevice.py | 4 ++-- cura/PrinterOutput/PrinterOutputDevice.py | 21 ++++++++++++++++++- cura/Settings/MachineManager.py | 11 ++++------ .../src/Cloud/CloudOutputDevice.py | 16 +++----------- .../UltimakerNetworkedPrinterOutputDevice.py | 4 ++-- .../qml/PrinterSelector/MachineSelector.qml | 4 ++-- 6 files changed, 33 insertions(+), 27 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 3dc245d468..1d0be1389e 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -33,8 +33,8 @@ class AuthState(IntEnum): class NetworkedPrinterOutputDevice(PrinterOutputDevice): authenticationStateChanged = pyqtSignal() - def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], connection_type: ConnectionType = ConnectionType.NetworkConnection, parent: QObject = None) -> None: - super().__init__(device_id = device_id, connection_type = connection_type, parent = parent) + def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], connection_type: ConnectionType = ConnectionType.NetworkConnection, parent: QObject = None, active: bool = True) -> None: + super().__init__(device_id = device_id, connection_type = connection_type, parent = parent, active = active) self._manager = None # type: Optional[QNetworkAccessManager] self._timeout_time = 10 # After how many seconds of no response should a timeout occur? diff --git a/cura/PrinterOutput/PrinterOutputDevice.py b/cura/PrinterOutput/PrinterOutputDevice.py index 9c1727f569..b369fc1129 100644 --- a/cura/PrinterOutput/PrinterOutputDevice.py +++ b/cura/PrinterOutput/PrinterOutputDevice.py @@ -72,7 +72,10 @@ class PrinterOutputDevice(QObject, OutputDevice): # Signal to indicate that the configuration of one of the printers has changed. uniqueConfigurationsChanged = pyqtSignal() - def __init__(self, device_id: str, connection_type: "ConnectionType" = ConnectionType.NotConnected, parent: QObject = None) -> None: + # Signal to indicate that the printer has become active or inactive + activeChanged = pyqtSignal() + + def __init__(self, device_id: str, connection_type: "ConnectionType" = ConnectionType.NotConnected, parent: QObject = None, active: bool = True) -> None: super().__init__(device_id = device_id, parent = parent) # type: ignore # MyPy complains with the multiple inheritance self._printers = [] # type: List[PrinterOutputModel] @@ -88,6 +91,8 @@ class PrinterOutputDevice(QObject, OutputDevice): self._accepts_commands = False # type: bool + self._active: bool = active + self._update_timer = QTimer() # type: QTimer self._update_timer.setInterval(2000) # TODO; Add preference for update interval self._update_timer.setSingleShot(False) @@ -295,3 +300,17 @@ class PrinterOutputDevice(QObject, OutputDevice): return self._firmware_updater.updateFirmware(firmware_file) + + @pyqtProperty(bool, notify = activeChanged) + def active(self) -> bool: + """ + Indicates whether the printer is active, which is not the same as "being the active printer". In this case, + active means that the printer can be used. An example of an inactive printer is one that cannot be used because + the user doesn't have enough seats on Digital Factory. + """ + return self._active + + def _setActive(self, active: bool) -> None: + if active != self._active: + self._active = active + self.activeChanged.emit() diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 1bdb32f4ac..3a2201449d 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -184,15 +184,13 @@ class MachineManager(QObject): def _onOutputDevicesChanged(self) -> None: for printer_output_device in self._printer_output_devices: - if hasattr(printer_output_device, "cloudActiveChanged"): - printer_output_device.cloudActiveChanged.disconnect(self.printerConnectedStatusChanged) + printer_output_device.activeChanged.disconnect(self.printerConnectedStatusChanged) self._printer_output_devices = [] for printer_output_device in self._application.getOutputDeviceManager().getOutputDevices(): if isinstance(printer_output_device, PrinterOutputDevice): self._printer_output_devices.append(printer_output_device) - if hasattr(printer_output_device, "cloudActiveChanged"): - printer_output_device.cloudActiveChanged.connect(self.printerConnectedStatusChanged) + printer_output_device.activeChanged.connect(self.printerConnectedStatusChanged) self.outputDevicesChanged.emit() @@ -576,12 +574,11 @@ class MachineManager(QObject): return self.activeMachineHasCloudConnection and not self.activeMachineHasNetworkConnection @pyqtProperty(bool, notify = printerConnectedStatusChanged) - def activeMachineIsCloudActive(self) -> bool: + def activeMachineIsActive(self) -> bool: if not self._printer_output_devices: return True - first_printer = self._printer_output_devices[0] - return True if not hasattr(first_printer, 'cloudActive') else first_printer.cloudActive + return self._printer_output_devices[0].active def activeMachineNetworkKey(self) -> str: if self._global_container_stack: diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index 020cafacd8..010ef93fbd 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -65,8 +65,6 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): # Therefore, we create a private signal used to trigger the printersChanged signal. _cloudClusterPrintersChanged = pyqtSignal() - cloudActiveChanged = pyqtSignal() - def __init__(self, api_client: CloudApiClient, cluster: CloudClusterResponse, parent: QObject = None) -> None: """Creates a new cloud output device @@ -91,7 +89,8 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): address="", connection_type=ConnectionType.CloudConnection, properties=properties, - parent=parent + parent=parent, + active=cluster.display_status != "inactive" ) self._api = api_client @@ -115,9 +114,6 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._pre_upload_print_job = None # type: Optional[CloudPrintJobResponse] self._uploaded_print_job = None # type: Optional[CloudPrintJobResponse] - # Whether the printer is active, i.e. authorized for use i.r.t to workspace limitations - self._active = cluster.display_status != "inactive" - CuraApplication.getInstance().getBackend().backendDone.connect(self._resetPrintJob) CuraApplication.getInstance().getController().getScene().sceneChanged.connect(self._onSceneChanged) @@ -197,9 +193,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._received_print_jobs = status.print_jobs self._updatePrintJobs(status.print_jobs) - if status.active != self._active: - self._active = status.active - self.cloudActiveChanged.emit() + self._setActive(status.active) def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, filter_by_machine: bool = False, **kwargs) -> None: @@ -445,10 +439,6 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): root_url_prefix = "-staging" if self._account.is_staging else "" return f"https://digitalfactory{root_url_prefix}.ultimaker.com/app/jobs/{self.clusterData.cluster_id}" - @pyqtProperty(bool, notify = cloudActiveChanged) - def cloudActive(self) -> bool: - return self._active - def __del__(self): CuraApplication.getInstance().getBackend().backendDone.disconnect(self._resetPrintJob) CuraApplication.getInstance().getController().getScene().sceneChanged.disconnect(self._onSceneChanged) diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index 8f25df37db..3ac5ccc7e7 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -46,10 +46,10 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): QUEUED_PRINT_JOBS_STATES = {"queued", "error"} def __init__(self, device_id: str, address: str, properties: Dict[bytes, bytes], connection_type: ConnectionType, - parent=None) -> None: + parent=None, active: bool = True) -> None: super().__init__(device_id=device_id, address=address, properties=properties, connection_type=connection_type, - parent=parent) + parent=parent, active=active) # Trigger the printersChanged signal when the private signal is triggered. self.printersChanged.connect(self._clusterPrintersChanged) diff --git a/resources/qml/PrinterSelector/MachineSelector.qml b/resources/qml/PrinterSelector/MachineSelector.qml index 7acdd9573b..e8ee98fe8f 100644 --- a/resources/qml/PrinterSelector/MachineSelector.qml +++ b/resources/qml/PrinterSelector/MachineSelector.qml @@ -16,7 +16,7 @@ Cura.ExpandablePopup property bool isConnectedCloudPrinter: machineManager.activeMachineHasCloudConnection property bool isCloudRegistered: machineManager.activeMachineHasCloudRegistration property bool isGroup: machineManager.activeMachineIsGroup - property bool isCloudActive: machineManager.activeMachineIsCloudActive + property bool isActive: machineManager.activeMachineIsActive property string machineName: { if (isNetworkPrinter && machineManager.activeMachineNetworkGroupName != "") { @@ -41,7 +41,7 @@ Cura.ExpandablePopup } else if (isConnectedCloudPrinter && Cura.API.connectionStatus.isInternetReachable) { - if (isCloudActive) + if (isActive) { return "printer_cloud_connected" } From bbddcab4e9520af04e390dc7c9ffe739488ddef1 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 2 Jul 2025 14:04:41 +0200 Subject: [PATCH 097/159] Proper paint-on-seam UI CURA-12578 --- plugins/PaintTool/BrushColorButton.qml | 25 ++ plugins/PaintTool/BrushShapeButton.qml | 25 ++ plugins/PaintTool/PaintModeButton.qml | 24 ++ plugins/PaintTool/PaintTool.py | 19 +- plugins/PaintTool/PaintTool.qml | 264 +++++++++--------- plugins/PaintTool/PaintView.py | 38 ++- ...ectorButton.qml => ModeSelectorButton.qml} | 13 +- .../RecommendedQualityProfileSelector.qml | 5 +- .../cura-light/icons/default/Circle.svg | 5 + .../cura-light/icons/default/Eraser.svg | 5 + .../themes/cura-light/icons/default/Seam.svg | 6 + .../cura-light/icons/low/CancelBadge.svg | 5 + .../cura-light/icons/low/CheckBadge.svg | 5 + 13 files changed, 274 insertions(+), 165 deletions(-) create mode 100644 plugins/PaintTool/BrushColorButton.qml create mode 100644 plugins/PaintTool/BrushShapeButton.qml create mode 100644 plugins/PaintTool/PaintModeButton.qml rename resources/qml/{PrintSetupSelector/Recommended/RecommendedQualityProfileSelectorButton.qml => ModeSelectorButton.qml} (91%) create mode 100644 resources/themes/cura-light/icons/default/Circle.svg create mode 100644 resources/themes/cura-light/icons/default/Eraser.svg create mode 100644 resources/themes/cura-light/icons/default/Seam.svg create mode 100644 resources/themes/cura-light/icons/low/CancelBadge.svg create mode 100644 resources/themes/cura-light/icons/low/CheckBadge.svg diff --git a/plugins/PaintTool/BrushColorButton.qml b/plugins/PaintTool/BrushColorButton.qml new file mode 100644 index 0000000000..71556f2681 --- /dev/null +++ b/plugins/PaintTool/BrushColorButton.qml @@ -0,0 +1,25 @@ +// Copyright (c) 2025 UltiMaker +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick + +import UM 1.7 as UM +import Cura 1.0 as Cura + + +UM.ToolbarButton +{ + id: buttonBrushColor + + property string color + + checked: base.selectedColor === buttonBrushColor.color + + onClicked: setColor() + + function setColor() + { + base.selectedColor = buttonBrushColor.color + UM.Controller.triggerActionWithData("setBrushColor", buttonBrushColor.color) + } +} diff --git a/plugins/PaintTool/BrushShapeButton.qml b/plugins/PaintTool/BrushShapeButton.qml new file mode 100644 index 0000000000..5c290e4a13 --- /dev/null +++ b/plugins/PaintTool/BrushShapeButton.qml @@ -0,0 +1,25 @@ +// Copyright (c) 2025 UltiMaker +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick + +import UM 1.7 as UM +import Cura 1.0 as Cura + + +UM.ToolbarButton +{ + id: buttonBrushShape + + property int shape + + checked: base.selectedShape === buttonBrushShape.shape + + onClicked: setShape() + + function setShape() + { + base.selectedShape = buttonBrushShape.shape + UM.Controller.triggerActionWithData("setBrushShape", buttonBrushShape.shape) + } +} diff --git a/plugins/PaintTool/PaintModeButton.qml b/plugins/PaintTool/PaintModeButton.qml new file mode 100644 index 0000000000..473996e04b --- /dev/null +++ b/plugins/PaintTool/PaintModeButton.qml @@ -0,0 +1,24 @@ +// Copyright (c) 2025 UltiMaker +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick + +import UM 1.7 as UM +import Cura 1.0 as Cura + +Cura.ModeSelectorButton +{ + id: modeSelectorButton + + property string mode + + selected: base.selectedMode === modeSelectorButton.mode + + onClicked: setMode() + + function setMode() + { + base.selectedMode = modeSelectorButton.mode + UM.Controller.triggerActionWithData("setPaintType", modeSelectorButton.mode) + } +} diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index 0c3ac0d661..524011af9d 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -42,7 +42,7 @@ class PaintTool(Tool): self._cache_dirty: bool = True self._brush_size: int = 10 - self._brush_color: str = "A" + self._brush_color: str = "" self._brush_shape: PaintTool.Brush.Shape = PaintTool.Brush.Shape.SQUARE self._brush_pen: QPen = self._createBrushPen() @@ -122,6 +122,18 @@ class PaintTool(Tool): self._updateScene() return True + def clear(self) -> None: + paintview = self._get_paint_view() + if paintview is None: + return + + width, height = paintview.getUvTexDimensions() + clear_image = QImage(width, height, QImage.Format.Format_RGB32) + clear_image.fill(Qt.GlobalColor.white) + paintview.addStroke(clear_image, 0, 0, "none") + + self._updateScene() + @staticmethod def _get_paint_view() -> Optional[PaintView]: paint_view = Application.getInstance().getController().getActiveView() @@ -265,10 +277,9 @@ class PaintTool(Tool): else: self._mouse_held = True - paintview = controller.getActiveView() - if paintview is None or paintview.getPluginId() != "PaintTool": + paintview = self._get_paint_view() + if paintview is None: return False - paintview = cast(PaintView, paintview) if not self._selection_pass: return False diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml index 602805cba1..4cbe9d4ade 100644 --- a/plugins/PaintTool/PaintTool.qml +++ b/plugins/PaintTool/PaintTool.qml @@ -15,6 +15,10 @@ Item height: childrenRect.height UM.I18nCatalog { id: catalog; name: "cura"} + property string selectedMode: "" + property string selectedColor: "" + property int selectedShape: 0 + Action { id: undoAction @@ -29,170 +33,158 @@ Item onTriggered: UM.Controller.triggerActionWithData("undoStackAction", true) } - ColumnLayout + Column { + id: mainColumn + spacing: UM.Theme.getSize("default_margin").height + RowLayout { - UM.ToolbarButton + id: rowPaintMode + width: parent.width + + PaintModeButton { - id: paintTypeA - - text: catalog.i18nc("@action:button", "Paint Type A") - toolItem: UM.ColorImage - { - source: UM.Theme.getIcon("Buildplate") - color: UM.Theme.getColor("icon") - } - property bool needBorder: true - - z: 2 - - onClicked: UM.Controller.triggerActionWithData("setPaintType", "A") + text: catalog.i18nc("@action:button", "Seam") + icon: "Seam" + tooltipText: catalog.i18nc("@tooltip", "Refine seam placement by defining preferred/avoidance areas") + mode: "seam" } - UM.ToolbarButton + PaintModeButton { - id: paintTypeB + text: catalog.i18nc("@action:button", "Support") + icon: "Support" + tooltipText: catalog.i18nc("@tooltip", "Refine support placement by defining preferred/avoidance areas") + mode: "support" + } + } - text: catalog.i18nc("@action:button", "Paint Type B") + //Line between the sections. + Rectangle + { + width: parent.width + height: UM.Theme.getSize("default_lining").height + color: UM.Theme.getColor("lining") + } + + RowLayout + { + id: rowBrushColor + + UM.Label + { + text: catalog.i18nc("@label", "Mark as") + } + + BrushColorButton + { + id: buttonPreferredArea + color: "preferred" + + text: catalog.i18nc("@action:button", "Preferred") toolItem: UM.ColorImage { - source: UM.Theme.getIcon("BlackMagic") + source: UM.Theme.getIcon("CheckBadge", "low") + color: UM.Theme.getColor("paint_preferred_area") + } + } + + BrushColorButton + { + id: buttonAvoidArea + color: "avoid" + + text: catalog.i18nc("@action:button", "Avoid") + toolItem: UM.ColorImage + { + source: UM.Theme.getIcon("CancelBadge", "low") + color: UM.Theme.getColor("paint_avoid_area") + } + } + + BrushColorButton + { + id: buttonEraseArea + color: "none" + + text: catalog.i18nc("@action:button", "Erase") + toolItem: UM.ColorImage + { + source: UM.Theme.getIcon("Eraser") color: UM.Theme.getColor("icon") } - property bool needBorder: true - - z: 2 - - onClicked: UM.Controller.triggerActionWithData("setPaintType", "B") } } RowLayout { - UM.ToolbarButton + id: rowBrushShape + + UM.Label { - id: colorButtonA - - text: catalog.i18nc("@action:button", "Color A") - toolItem: UM.ColorImage - { - source: UM.Theme.getIcon("Eye") - color: "purple" - } - property bool needBorder: true - - z: 2 - - onClicked: UM.Controller.triggerActionWithData("setBrushColor", "A") + text: catalog.i18nc("@label", "Brush Shape") } - UM.ToolbarButton + BrushShapeButton { - id: colorButtonB + id: buttonBrushCircle + shape: Cura.PaintToolBrush.CIRCLE - text: catalog.i18nc("@action:button", "Color B") + text: catalog.i18nc("@action:button", "Circle") toolItem: UM.ColorImage { - source: UM.Theme.getIcon("Eye") - color: "orange" + source: UM.Theme.getIcon("Circle") + color: UM.Theme.getColor("icon") } - property bool needBorder: true - - z: 2 - - onClicked: UM.Controller.triggerActionWithData("setBrushColor", "B") } - UM.ToolbarButton + BrushShapeButton { - id: colorButtonC + id: buttonBrushSquare + shape: Cura.PaintToolBrush.SQUARE - text: catalog.i18nc("@action:button", "Color C") - toolItem: UM.ColorImage - { - source: UM.Theme.getIcon("Eye") - color: "green" - } - property bool needBorder: true - - z: 2 - - onClicked: UM.Controller.triggerActionWithData("setBrushColor", "C") - } - - UM.ToolbarButton - { - id: colorButtonD - - text: catalog.i18nc("@action:button", "Color D") - toolItem: UM.ColorImage - { - source: UM.Theme.getIcon("Eye") - color: "ghostwhite" - } - property bool needBorder: true - - z: 2 - - onClicked: UM.Controller.triggerActionWithData("setBrushColor", "D") - } - } - - RowLayout - { - UM.ToolbarButton - { - id: shapeSquareButton - - text: catalog.i18nc("@action:button", "Square Brush") + text: catalog.i18nc("@action:button", "Square") toolItem: UM.ColorImage { source: UM.Theme.getIcon("MeshTypeNormal") color: UM.Theme.getColor("icon") } - property bool needBorder: true - - z: 2 - - onClicked: UM.Controller.triggerActionWithData("setBrushShape", Cura.PaintToolBrush.SQUARE) } + } - UM.ToolbarButton + UM.Label + { + text: catalog.i18nc("@label", "Brush Size") + } + + UM.Slider + { + id: shapeSizeSlider + width: parent.width + indicatorVisible: false + + from: 1 + to: 40 + value: 10 + + onPressedChanged: function(pressed) { - id: shapeCircleButton - - text: catalog.i18nc("@action:button", "Round Brush") - toolItem: UM.ColorImage + if(! pressed) { - source: UM.Theme.getIcon("CircleOutline") - color: UM.Theme.getColor("icon") - } - property bool needBorder: true - - z: 2 - - onClicked: UM.Controller.triggerActionWithData("setBrushShape", Cura.PaintToolBrush.CIRCLE) - } - - UM.Slider - { - id: shapeSizeSlider - - from: 1 - to: 40 - value: 10 - - onPressedChanged: function(pressed) - { - if(! pressed) - { - UM.Controller.triggerActionWithData("setBrushSize", shapeSizeSlider.value) - } + UM.Controller.triggerActionWithData("setBrushSize", shapeSizeSlider.value) } } } + //Line between the sections. + Rectangle + { + width: parent.width + height: UM.Theme.getSize("default_lining").height + color: UM.Theme.getColor("lining") + } + RowLayout { UM.ToolbarButton @@ -203,10 +195,8 @@ Item toolItem: UM.ColorImage { source: UM.Theme.getIcon("ArrowReset") + color: UM.Theme.getColor("icon") } - property bool needBorder: true - - z: 2 onClicked: undoAction.trigger() } @@ -218,14 +208,30 @@ Item text: catalog.i18nc("@action:button", "Redo Stroke") toolItem: UM.ColorImage { - source: UM.Theme.getIcon("ArrowDoubleCircleRight") + source: UM.Theme.getIcon("ArrowReset") + color: UM.Theme.getColor("icon") + transform: [ + Scale { xScale: -1; origin.x: width/2 } + ] } - property bool needBorder: true - - z: 2 onClicked: redoAction.trigger() } + + Cura.SecondaryButton + { + id: clearButton + text: catalog.i18nc("@button", "Clear all") + onClicked: UM.Controller.triggerAction("clear") + } } } + + Component.onCompleted: + { + // Force first types for consistency, otherwise UI may become different from controller + rowPaintMode.children[0].setMode() + rowBrushColor.children[1].setColor() + rowBrushShape.children[1].setShape() + } } diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index 32124872c4..22eb8c55f6 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -26,23 +26,17 @@ class PaintView(View): UNDO_STACK_SIZE = 1024 class PaintType: - def __init__(self, icon: str, display_color: Color, value: int): - self.icon: str = icon + def __init__(self, display_color: Color, value: int): self.display_color: Color = display_color self.value: int = value - class PaintMode: - def __init__(self, icon: str, types: Dict[str, "PaintView.PaintType"]): - self.icon: str = icon - self.types = types - def __init__(self) -> None: super().__init__() 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, PaintView.PaintMode] = {} + 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]] = [] @@ -54,12 +48,12 @@ class PaintView(View): def _makePaintModes(self): theme = CuraApplication.getInstance().getTheme() - usual_types = {"A": self.PaintType("Buildplate", Color(*theme.getColor("paint_normal_area").getRgb()), 0), - "B": self.PaintType("BlackMagic", Color(*theme.getColor("paint_preferred_area").getRgb()), 1), - "C": self.PaintType("Eye", Color(*theme.getColor("paint_avoid_area").getRgb()), 2)} + usual_types = {"none": self.PaintType(Color(*theme.getColor("paint_normal_area").getRgb()), 0), + "preferred": self.PaintType(Color(*theme.getColor("paint_preferred_area").getRgb()), 1), + "avoid": self.PaintType(Color(*theme.getColor("paint_avoid_area").getRgb()), 2)} self._paint_modes = { - "A": self.PaintMode("MeshTypeNormal", usual_types), - "B": self.PaintMode("CircleOutline", usual_types), + "seam": usual_types, + "support": usual_types, } def _checkSetup(self): @@ -78,32 +72,32 @@ class PaintView(View): res.setAlphaChannel(self._force_opaque_mask.scaled(image.width(), image.height())) return res - def addStroke(self, stroke_image: 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) -> None: if self._current_paint_texture is None or self._current_paint_texture.getImage() is None: return actual_image = self._current_paint_texture.getImage() bit_range_start, bit_range_end = self._current_bits_ranges - set_value = self._paint_modes[self._current_paint_type].types[brush_color].value << self._current_bits_ranges[0] + 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_image.width(), stroke_image.height()) + image_rect = QRect(0, 0, stroke_mask.width(), stroke_mask.height()) - clear_bits_image = stroke_image.copy() + 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() - set_value_image = stroke_image.copy() + 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() - stroked_image = actual_image.copy(start_x, start_y, stroke_image.width(), stroke_image.height()) + 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) @@ -149,7 +143,7 @@ class PaintView(View): 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].types)) + new_mapping = self._add_mapping(paint_data_mapping, len(self._paint_modes[paint_type])) paint_data_mapping[paint_type] = new_mapping node.callDecoration("setTextureDataMapping", paint_data_mapping) @@ -177,12 +171,12 @@ class PaintView(View): return if self._current_paint_type == "": - self.setPaintType("A") + return self._paint_shader.setUniformValue("u_bitsRangesStart", self._current_bits_ranges[0]) self._paint_shader.setUniformValue("u_bitsRangesEnd", self._current_bits_ranges[1]) - colors = [paint_type_obj.display_color for paint_type_obj in self._paint_modes[self._current_paint_type].types.values()] + colors = [paint_type_obj.display_color for paint_type_obj in self._paint_modes[self._current_paint_type].values()] colors_values = [[int(color_part * 255) for color_part in [color.r, color.g, color.b]] for color in colors] self._paint_shader.setUniformValueArray("u_renderColors", colors_values) diff --git a/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelectorButton.qml b/resources/qml/ModeSelectorButton.qml similarity index 91% rename from resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelectorButton.qml rename to resources/qml/ModeSelectorButton.qml index 1bbc726b9d..65a6ee4a75 100644 --- a/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelectorButton.qml +++ b/resources/qml/ModeSelectorButton.qml @@ -17,7 +17,7 @@ Rectangle color: mouseArea.containsMouse || selected ? UM.Theme.getColor("background_3") : UM.Theme.getColor("background_1") property bool selected: false - property string profileName: "" + property alias text: mainLabel.text property string icon: "" property string custom_icon: "" property alias tooltipText: tooltip.text @@ -42,18 +42,18 @@ Rectangle Item { - width: intentIcon.width + width: mainIcon.width anchors { top: parent.top - bottom: qualityLabel.top + bottom: mainLabel.top horizontalCenter: parent.horizontalCenter topMargin: UM.Theme.getSize("narrow_margin").height } Item { - id: intentIcon + id: mainIcon width: UM.Theme.getSize("recommended_button_icon").width height: UM.Theme.getSize("recommended_button_icon").height @@ -90,7 +90,7 @@ Rectangle { id: initialLabel anchors.centerIn: parent - text: profileName.charAt(0).toUpperCase() + text: base.text.charAt(0).toUpperCase() font: UM.Theme.getFont("small_bold") horizontalAlignment: Text.AlignHCenter } @@ -102,8 +102,7 @@ Rectangle UM.Label { - id: qualityLabel - text: profileName + id: mainLabel anchors { bottom: parent.bottom diff --git a/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml b/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml index 19c57e5130..1559f6cec3 100644 --- a/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml +++ b/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml @@ -7,7 +7,6 @@ import QtQuick.Layouts 2.10 import UM 1.5 as UM import Cura 1.7 as Cura -import ".." Item { @@ -28,9 +27,9 @@ Item id: intentSelectionRepeater model: Cura.IntentSelectionModel {} - RecommendedQualityProfileSelectorButton + Cura.ModeSelectorButton { - profileName: model.name + text: model.name icon: model.icon ? model.icon : "" custom_icon: model.custom_icon ? model.custom_icon : "" tooltipText: model.description ? model.description : "" diff --git a/resources/themes/cura-light/icons/default/Circle.svg b/resources/themes/cura-light/icons/default/Circle.svg new file mode 100644 index 0000000000..c69b5a4e31 --- /dev/null +++ b/resources/themes/cura-light/icons/default/Circle.svg @@ -0,0 +1,5 @@ + + + + diff --git a/resources/themes/cura-light/icons/default/Eraser.svg b/resources/themes/cura-light/icons/default/Eraser.svg new file mode 100644 index 0000000000..fbe5103993 --- /dev/null +++ b/resources/themes/cura-light/icons/default/Eraser.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/themes/cura-light/icons/default/Seam.svg b/resources/themes/cura-light/icons/default/Seam.svg new file mode 100644 index 0000000000..a9615832d6 --- /dev/null +++ b/resources/themes/cura-light/icons/default/Seam.svg @@ -0,0 +1,6 @@ + + + + diff --git a/resources/themes/cura-light/icons/low/CancelBadge.svg b/resources/themes/cura-light/icons/low/CancelBadge.svg new file mode 100644 index 0000000000..25c4198083 --- /dev/null +++ b/resources/themes/cura-light/icons/low/CancelBadge.svg @@ -0,0 +1,5 @@ + + + + diff --git a/resources/themes/cura-light/icons/low/CheckBadge.svg b/resources/themes/cura-light/icons/low/CheckBadge.svg new file mode 100644 index 0000000000..a10a92c6af --- /dev/null +++ b/resources/themes/cura-light/icons/low/CheckBadge.svg @@ -0,0 +1,5 @@ + + + + From 17bd53e0497e56d8ee6285ccc504d4b1fab13480 Mon Sep 17 00:00:00 2001 From: Paul Kuiper <46715907+pkuiper-ultimaker@users.noreply.github.com> Date: Fri, 27 Jun 2025 10:53:49 +0200 Subject: [PATCH 098/159] Add CC+0.6 core and print profiles. Allow PC, CPE+ and Breakaway on AA+0.4. PP-641 --- ...us_0.4_cpe-plus_0.2mm_engineering.inst.cfg | 18 +++++++++++ ..._aa_plus_0.4_pc_0.2mm_engineering.inst.cfg | 18 +++++++++++ ...us_0.4_cpe-plus_0.2mm_engineering.inst.cfg | 1 + ..._nylon-cf-slide_0.2mm_engineering.inst.cfg | 1 + ..._cc_plus_0.4_pc_0.2mm_engineering.inst.cfg | 1 + ..._plus_0.4_petcf_0.2mm_engineering.inst.cfg | 1 + ..._nylon-cf-slide_0.2mm_engineering.inst.cfg | 18 +++++++++++ ..._plus_0.6_petcf_0.2mm_engineering.inst.cfg | 18 +++++++++++ .../um_s8_aa_plus_0.4_bam_0.15mm.inst.cfg | 32 +++++++++++++++++++ .../um_s8_aa_plus_0.4_bam_0.1mm.inst.cfg | 28 ++++++++++++++++ .../um_s8_aa_plus_0.4_bam_0.2mm.inst.cfg | 31 ++++++++++++++++++ .../um_s8_aa_plus_0.4_bam_0.3mm.inst.cfg | 31 ++++++++++++++++++ .../um_s8_aa_plus_0.4_cpe-plus_0.2mm.inst.cfg | 19 +++++++++++ .../um_s8_aa_plus_0.4_pc_0.2mm.inst.cfg | 25 +++++++++++++++ .../um_s8_aa_plus_0.4_petg_0.2mm.inst.cfg | 1 + .../um_s8_cc_plus_0.4_cpe-plus_0.2mm.inst.cfg | 3 +- ..._cc_plus_0.4_nylon-cf-slide_0.2mm.inst.cfg | 9 +++++- .../um_s8_cc_plus_0.4_pc_0.2mm.inst.cfg | 3 +- .../um_s8_cc_plus_0.4_petcf_0.2mm.inst.cfg | 10 +++++- ..._cc_plus_0.6_nylon-cf-slide_0.2mm.inst.cfg | 26 +++++++++++++++ ..._cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg | 27 ++++++++++++++++ .../um_s8_cc_plus_0.6_petcf_0.2mm.inst.cfg | 27 ++++++++++++++++ .../um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg | 30 +++++++++++++++++ .../variants/ultimaker_s6_cc_plus06.inst.cfg | 17 ++++++++++ .../variants/ultimaker_s8_cc_plus06.inst.cfg | 17 ++++++++++ 25 files changed, 408 insertions(+), 4 deletions(-) create mode 100644 resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg create mode 100644 resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm_engineering.inst.cfg create mode 100644 resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg create mode 100644 resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.15mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.1mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.2mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.3mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg create mode 100644 resources/variants/ultimaker_s6_cc_plus06.inst.cfg create mode 100644 resources/variants/ultimaker_s8_cc_plus06.inst.cfg diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg new file mode 100644 index 0000000000..21e814e112 --- /dev/null +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg @@ -0,0 +1,18 @@ +[general] +definition = ultimaker_s8 +name = Accurate +version = 4 + +[metadata] +intent_category = engineering +material = generic_cpe_plus +quality_type = draft +setting_version = 25 +type = intent +variant = AA+ 0.4 + +[values] +infill_sparse_density = 20 +top_bottom_thickness = =wall_thickness +wall_thickness = =line_width * 4 + diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm_engineering.inst.cfg new file mode 100644 index 0000000000..8332ecacbc --- /dev/null +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm_engineering.inst.cfg @@ -0,0 +1,18 @@ +[general] +definition = ultimaker_s8 +name = Accurate +version = 4 + +[metadata] +intent_category = engineering +material = generic_pc +quality_type = draft +setting_version = 25 +type = intent +variant = AA+ 0.4 + +[values] +infill_sparse_density = 20 +top_bottom_thickness = =wall_thickness +wall_thickness = =line_width * 4 + diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg index a474c19cf7..832912a022 100644 --- a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm_engineering.inst.cfg @@ -5,6 +5,7 @@ version = 4 [metadata] intent_category = engineering +is_experimental = True material = generic_cpe_plus quality_type = draft setting_version = 25 diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm_engineering.inst.cfg index e5ccc32e49..a0e65969e9 100644 --- a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm_engineering.inst.cfg @@ -5,6 +5,7 @@ version = 4 [metadata] intent_category = engineering +is_experimental = True material = generic_nylon-cf-slide quality_type = draft setting_version = 25 diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm_engineering.inst.cfg index cecb670819..f40d2509b6 100644 --- a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm_engineering.inst.cfg @@ -5,6 +5,7 @@ version = 4 [metadata] intent_category = engineering +is_experimental = True material = generic_pc quality_type = draft setting_version = 25 diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm_engineering.inst.cfg index 0819ec9c81..824018a1d5 100644 --- a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm_engineering.inst.cfg @@ -5,6 +5,7 @@ version = 4 [metadata] intent_category = engineering +is_experimental = True material = generic_petcf quality_type = draft setting_version = 25 diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg new file mode 100644 index 0000000000..8cbb513108 --- /dev/null +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg @@ -0,0 +1,18 @@ +[general] +definition = ultimaker_s8 +name = Accurate +version = 4 + +[metadata] +intent_category = engineering +material = generic_nylon-cf-slide +quality_type = draft +setting_version = 25 +type = intent +variant = CC+ 0.6 + +[values] +infill_sparse_density = 20 +top_bottom_thickness = =wall_thickness +wall_thickness = =line_width * 4 + diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg new file mode 100644 index 0000000000..5384f380ac --- /dev/null +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg @@ -0,0 +1,18 @@ +[general] +definition = ultimaker_s8 +name = Accurate +version = 4 + +[metadata] +intent_category = engineering +material = generic_petcf +quality_type = draft +setting_version = 25 +type = intent +variant = CC+ 0.6 + +[values] +infill_sparse_density = 20 +top_bottom_thickness = =wall_thickness +wall_thickness = =line_width * 4 + diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.15mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.15mm.inst.cfg new file mode 100644 index 0000000000..5dd8150520 --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.15mm.inst.cfg @@ -0,0 +1,32 @@ +[general] +definition = ultimaker_s8 +name = Normal +version = 4 + +[metadata] +material = generic_bam +quality_type = fast +setting_version = 25 +type = quality +variant = AA+ 0.4 +weight = -1 + +[values] +brim_replaces_support = False +build_volume_temperature = =50 if extruders_enabled_count > 1 and (not support_enable or extruder_nr != support_extruder_nr) else 24 +default_material_bed_temperature = =0 if extruders_enabled_count > 1 and (not support_enable or extruder_nr != support_extruder_nr) else 60 +machine_nozzle_cool_down_speed = 0.75 +machine_nozzle_heat_up_speed = 1.6 +prime_tower_enable = =min(extruderValues('material_surface_energy')) < 100 +speed_print = 80 +speed_topbottom = =math.ceil(speed_print * 30 / 80) +speed_wall = =math.ceil(speed_print * 40 / 80) +speed_wall_0 = =math.ceil(speed_wall * 30 / 40) +support_angle = 45 +support_bottom_distance = =math.ceil(min(extruderValues('material_adhesion_tendency')) / 2) * layer_height +support_infill_sparse_thickness = =2 * layer_height +support_interface_density = =min(extruderValues('material_surface_energy')) +support_interface_enable = True +support_top_distance = =math.ceil(min(extruderValues('material_adhesion_tendency')) / 1) * layer_height +top_bottom_thickness = 1 + diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.1mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.1mm.inst.cfg new file mode 100644 index 0000000000..9972855717 --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.1mm.inst.cfg @@ -0,0 +1,28 @@ +[general] +definition = ultimaker_s8 +name = Fine +version = 4 + +[metadata] +material = generic_bam +quality_type = normal +setting_version = 25 +type = quality +variant = AA+ 0.4 +weight = 0 + +[values] +brim_replaces_support = False +build_volume_temperature = =50 if extruders_enabled_count > 1 and (not support_enable or extruder_nr != support_extruder_nr) else 24 +default_material_bed_temperature = =0 if extruders_enabled_count > 1 and (not support_enable or extruder_nr != support_extruder_nr) else 60 +machine_nozzle_cool_down_speed = 0.75 +machine_nozzle_heat_up_speed = 1.6 +prime_tower_enable = =min(extruderValues('material_surface_energy')) < 100 +support_angle = 45 +support_bottom_distance = =math.ceil(min(extruderValues('material_adhesion_tendency')) / 2) * layer_height +support_infill_sparse_thickness = =2 * layer_height +support_interface_density = =min(extruderValues('material_surface_energy')) +support_interface_enable = True +support_top_distance = =math.ceil(min(extruderValues('material_adhesion_tendency')) / 1) * layer_height +top_bottom_thickness = 1 + diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.2mm.inst.cfg new file mode 100644 index 0000000000..8425b1842d --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.2mm.inst.cfg @@ -0,0 +1,31 @@ +[general] +definition = ultimaker_s8 +name = Fast +version = 4 + +[metadata] +material = generic_bam +quality_type = draft +setting_version = 25 +type = quality +variant = AA+ 0.4 +weight = -2 + +[values] +brim_replaces_support = False +build_volume_temperature = =50 if extruders_enabled_count > 1 and (not support_enable or extruder_nr != support_extruder_nr) else 24 +default_material_bed_temperature = =0 if extruders_enabled_count > 1 and (not support_enable or extruder_nr != support_extruder_nr) else 60 +machine_nozzle_cool_down_speed = 0.75 +machine_nozzle_heat_up_speed = 1.6 +material_print_temperature = =default_material_print_temperature + 5 +prime_tower_enable = =min(extruderValues('material_surface_energy')) < 100 +speed_topbottom = =math.ceil(speed_print * 35 / 70) +speed_wall = =math.ceil(speed_print * 50 / 70) +speed_wall_0 = =math.ceil(speed_wall * 35 / 50) +support_angle = 45 +support_bottom_distance = =math.ceil(min(extruderValues('material_adhesion_tendency')) / 2) * layer_height +support_interface_density = =min(extruderValues('material_surface_energy')) +support_interface_enable = True +support_top_distance = =math.ceil(min(extruderValues('material_adhesion_tendency')) / 2) * layer_height +top_bottom_thickness = 1 + diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.3mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.3mm.inst.cfg new file mode 100644 index 0000000000..69ad43b578 --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.3mm.inst.cfg @@ -0,0 +1,31 @@ +[general] +definition = ultimaker_s8 +name = Extra Fast +version = 4 + +[metadata] +material = generic_bam +quality_type = verydraft +setting_version = 25 +type = quality +variant = AA+ 0.4 +weight = -3 + +[values] +brim_replaces_support = False +build_volume_temperature = =50 if extruders_enabled_count > 1 and (not support_enable or extruder_nr != support_extruder_nr) else 24 +default_material_bed_temperature = =0 if extruders_enabled_count > 1 and (not support_enable or extruder_nr != support_extruder_nr) else 60 +machine_nozzle_cool_down_speed = 0.75 +machine_nozzle_heat_up_speed = 1.6 +material_print_temperature = =default_material_print_temperature + 5 +prime_tower_enable = =min(extruderValues('material_surface_energy')) < 100 +speed_topbottom = =math.ceil(speed_print * 35 / 70) +speed_wall = =math.ceil(speed_print * 50 / 70) +speed_wall_0 = =math.ceil(speed_wall * 35 / 50) +support_angle = 45 +support_bottom_distance = 0.3 +support_interface_density = =min(extruderValues('material_surface_energy')) +support_interface_enable = True +support_top_distance = 0.3 +top_bottom_thickness = 1 + diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm.inst.cfg new file mode 100644 index 0000000000..9ca2677c01 --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_cpe-plus_0.2mm.inst.cfg @@ -0,0 +1,19 @@ +[general] +definition = ultimaker_s8 +name = Fast +version = 4 + +[metadata] +material = generic_cpe_plus +quality_type = draft +setting_version = 25 +type = quality +variant = AA+ 0.4 +weight = -2 + +[values] +adhesion_type = brim +material_alternate_walls = True +material_final_print_temperature = =material_print_temperature - 15 +material_initial_print_temperature = =material_print_temperature - 15 + diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm.inst.cfg new file mode 100644 index 0000000000..0709fe6c6c --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pc_0.2mm.inst.cfg @@ -0,0 +1,25 @@ +[general] +definition = ultimaker_s8 +name = Fast +version = 4 + +[metadata] +material = generic_pc +quality_type = draft +setting_version = 25 +type = quality +variant = AA+ 0.4 +weight = -2 + +[values] +adhesion_type = brim +cool_min_layer_time = 6 +cool_min_layer_time_fan_speed_max = 12 +inset_direction = inside_out +material_alternate_walls = True +material_final_print_temperature = =material_print_temperature - 15 +material_flow = 95 +material_initial_print_temperature = =material_print_temperature - 15 +retraction_prime_speed = 15 +speed_wall_x = =speed_wall_0 + diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm.inst.cfg index baabd79e94..a22e4fbeec 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm.inst.cfg @@ -15,4 +15,5 @@ weight = -2 cool_min_layer_time = 4 material_print_temperature = =default_material_print_temperature + 5 retraction_prime_speed = 15 +support_structure = tree diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm.inst.cfg index 3475b01999..80d65e7955 100644 --- a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_cpe-plus_0.2mm.inst.cfg @@ -1,9 +1,10 @@ [general] definition = ultimaker_s8 -name = Fast +name = Fast - Experimental version = 4 [metadata] +is_experimental = True material = generic_cpe_plus quality_type = draft setting_version = 25 diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm.inst.cfg index 1085302fc7..5f3180ef4e 100644 --- a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_nylon-cf-slide_0.2mm.inst.cfg @@ -1,9 +1,10 @@ [general] definition = ultimaker_s8 -name = Fast +name = Fast - Experimental version = 4 [metadata] +is_experimental = True material = generic_nylon-cf-slide quality_type = draft setting_version = 25 @@ -12,6 +13,12 @@ variant = CC+ 0.4 weight = -2 [values] +bridge_skin_material_flow = 100 +bridge_skin_speed = 30 +bridge_wall_material_flow = 100 +bridge_wall_speed = 30 cool_min_layer_time_fan_speed_max = 11 retraction_prime_speed = 15 +support_structure = tree +wall_overhang_speed_factors = [100,90,80,70,60,50] diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm.inst.cfg index bb73d83750..540d62e154 100644 --- a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm.inst.cfg @@ -1,9 +1,10 @@ [general] definition = ultimaker_s8 -name = Fast +name = Fast - Experimental version = 4 [metadata] +is_experimental = True material = generic_pc quality_type = draft setting_version = 25 diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm.inst.cfg index 0038bb0a4d..3467ed5ded 100644 --- a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_petcf_0.2mm.inst.cfg @@ -1,9 +1,10 @@ [general] definition = ultimaker_s8 -name = Fast +name = Fast - Experimental version = 4 [metadata] +is_experimental = True material = generic_petcf quality_type = draft setting_version = 25 @@ -12,5 +13,12 @@ variant = CC+ 0.4 weight = -2 [values] +adhesion_type = skirt +bridge_skin_material_flow = 100 +bridge_skin_speed = 30 +bridge_wall_material_flow = 100 +bridge_wall_speed = 30 +support_structure = tree switch_extruder_retraction_amount = 16 +wall_overhang_speed_factors = [100,90,80,70,60,50] diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm.inst.cfg new file mode 100644 index 0000000000..5741714558 --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm.inst.cfg @@ -0,0 +1,26 @@ +[general] +definition = ultimaker_s8 +name = Fast +version = 4 + +[metadata] +material = generic_nylon-cf-slide +quality_type = draft +setting_version = 25 +type = quality +variant = CC+ 0.6 +weight = -2 + +[values] +bridge_skin_material_flow = 100 +bridge_skin_speed = 30 +bridge_wall_material_flow = 100 +bridge_wall_speed = 30 +cool_min_layer_time_fan_speed_max = 11 +retraction_prime_speed = 15 +speed_print = 85 +speed_wall = =speed_print +support_line_width = 0.6 +support_structure = tree +wall_overhang_speed_factors = [100,90,80,70,60,50] + diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg new file mode 100644 index 0000000000..c8a58b55e7 --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg @@ -0,0 +1,27 @@ +[general] +definition = ultimaker_s8 +name = Extra Fast +version = 4 + +[metadata] +material = generic_nylon-cf-slide +quality_type = verydraft +setting_version = 25 +type = quality +variant = CC+ 0.6 +weight = -3 + +[values] +bridge_skin_material_flow = 100 +bridge_skin_speed = 30 +bridge_wall_material_flow = 100 +bridge_wall_speed = 30 +cool_min_layer_time_fan_speed_max = 11 +retraction_prime_speed = 15 +speed_print = 60 +speed_support_interface = 60 +speed_wall = =speed_print +support_line_width = 0.6 +support_structure = tree +wall_overhang_speed_factors = [100,90,80,70,60,50] + diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm.inst.cfg new file mode 100644 index 0000000000..803717ba02 --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm.inst.cfg @@ -0,0 +1,27 @@ +[general] +definition = ultimaker_s8 +name = Fast +version = 4 + +[metadata] +material = generic_petcf +quality_type = draft +setting_version = 25 +type = quality +variant = CC+ 0.6 +weight = -2 + +[values] +adhesion_type = skirt +bridge_skin_material_flow = 100 +bridge_skin_speed = 30 +bridge_wall_material_flow = 100 +bridge_wall_speed = 30 +speed_print = 85 +speed_wall = =speed_print +support_interface_enable = False +support_line_width = 0.6 +support_structure = tree +switch_extruder_retraction_amount = 16 +wall_overhang_speed_factors = [100,90,80,70,60,50] + diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg new file mode 100644 index 0000000000..8e8e243930 --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg @@ -0,0 +1,30 @@ +[general] +definition = ultimaker_s8 +name = Extra Fast +version = 4 + +[metadata] +material = generic_petcf +quality_type = verydraft +setting_version = 25 +type = quality +variant = CC+ 0.6 +weight = -3 + +[values] +adhesion_type = skirt +bridge_skin_material_flow = 100 +bridge_skin_speed = 30 +bridge_wall_material_flow = 100 +bridge_wall_speed = 30 +material_print_temperature = =default_material_print_temperature + 10 +speed_print = 60 +speed_support_interface = 60 +speed_wall = =speed_print +support_interface_enable = False +support_line_width = 0.6 +support_structure = tree +switch_extruder_retraction_amount = 16 +wall_line_width_0 = =line_width * (1 + magic_spiralize * 0.25) +wall_overhang_speed_factors = [100,90,80,70,60,50] + diff --git a/resources/variants/ultimaker_s6_cc_plus06.inst.cfg b/resources/variants/ultimaker_s6_cc_plus06.inst.cfg new file mode 100644 index 0000000000..93564bada0 --- /dev/null +++ b/resources/variants/ultimaker_s6_cc_plus06.inst.cfg @@ -0,0 +1,17 @@ +[general] +definition = ultimaker_s6 +name = CC+ 0.6 +version = 4 + +[metadata] +hardware_type = nozzle +setting_version = 25 +type = variant + +[values] +machine_nozzle_cool_down_speed = 0.9 +machine_nozzle_id = CC+ 0.6 +machine_nozzle_size = 0.6 +machine_nozzle_tip_outer_diameter = 1.2 +retraction_prime_speed = =retraction_speed + diff --git a/resources/variants/ultimaker_s8_cc_plus06.inst.cfg b/resources/variants/ultimaker_s8_cc_plus06.inst.cfg new file mode 100644 index 0000000000..2a1c43873f --- /dev/null +++ b/resources/variants/ultimaker_s8_cc_plus06.inst.cfg @@ -0,0 +1,17 @@ +[general] +definition = ultimaker_s8 +name = CC+ 0.6 +version = 4 + +[metadata] +hardware_type = nozzle +setting_version = 25 +type = variant + +[values] +machine_nozzle_cool_down_speed = 0.9 +machine_nozzle_id = CC+ 0.6 +machine_nozzle_size = 0.6 +machine_nozzle_tip_outer_diameter = 1.2 +retraction_prime_speed = =retraction_speed + From 7fe1da587882e210fea14016898e6214175e372b Mon Sep 17 00:00:00 2001 From: HellAholic Date: Thu, 3 Jul 2025 16:33:34 +0200 Subject: [PATCH 099/159] Remove the 0.3mm quality files --- ..._cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg | 27 ----------------- .../um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg | 30 ------------------- 2 files changed, 57 deletions(-) delete mode 100644 resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg delete mode 100644 resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg deleted file mode 100644 index c8a58b55e7..0000000000 --- a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg +++ /dev/null @@ -1,27 +0,0 @@ -[general] -definition = ultimaker_s8 -name = Extra Fast -version = 4 - -[metadata] -material = generic_nylon-cf-slide -quality_type = verydraft -setting_version = 25 -type = quality -variant = CC+ 0.6 -weight = -3 - -[values] -bridge_skin_material_flow = 100 -bridge_skin_speed = 30 -bridge_wall_material_flow = 100 -bridge_wall_speed = 30 -cool_min_layer_time_fan_speed_max = 11 -retraction_prime_speed = 15 -speed_print = 60 -speed_support_interface = 60 -speed_wall = =speed_print -support_line_width = 0.6 -support_structure = tree -wall_overhang_speed_factors = [100,90,80,70,60,50] - diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg deleted file mode 100644 index 8e8e243930..0000000000 --- a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg +++ /dev/null @@ -1,30 +0,0 @@ -[general] -definition = ultimaker_s8 -name = Extra Fast -version = 4 - -[metadata] -material = generic_petcf -quality_type = verydraft -setting_version = 25 -type = quality -variant = CC+ 0.6 -weight = -3 - -[values] -adhesion_type = skirt -bridge_skin_material_flow = 100 -bridge_skin_speed = 30 -bridge_wall_material_flow = 100 -bridge_wall_speed = 30 -material_print_temperature = =default_material_print_temperature + 10 -speed_print = 60 -speed_support_interface = 60 -speed_wall = =speed_print -support_interface_enable = False -support_line_width = 0.6 -support_structure = tree -switch_extruder_retraction_amount = 16 -wall_line_width_0 = =line_width * (1 + magic_spiralize * 0.25) -wall_overhang_speed_factors = [100,90,80,70,60,50] - From e90827a697d6917c9aa86a83955dbf1b0ce68e8c Mon Sep 17 00:00:00 2001 From: HellAholic Date: Thu, 3 Jul 2025 17:43:25 +0200 Subject: [PATCH 100/159] Revert BAM quality files --- .../um_s8_aa_plus_0.4_bam_0.15mm.inst.cfg | 32 ------------------- .../um_s8_aa_plus_0.4_bam_0.1mm.inst.cfg | 28 ---------------- .../um_s8_aa_plus_0.4_bam_0.2mm.inst.cfg | 31 ------------------ .../um_s8_aa_plus_0.4_bam_0.3mm.inst.cfg | 31 ------------------ 4 files changed, 122 deletions(-) delete mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.15mm.inst.cfg delete mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.1mm.inst.cfg delete mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.2mm.inst.cfg delete mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.3mm.inst.cfg diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.15mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.15mm.inst.cfg deleted file mode 100644 index 5dd8150520..0000000000 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.15mm.inst.cfg +++ /dev/null @@ -1,32 +0,0 @@ -[general] -definition = ultimaker_s8 -name = Normal -version = 4 - -[metadata] -material = generic_bam -quality_type = fast -setting_version = 25 -type = quality -variant = AA+ 0.4 -weight = -1 - -[values] -brim_replaces_support = False -build_volume_temperature = =50 if extruders_enabled_count > 1 and (not support_enable or extruder_nr != support_extruder_nr) else 24 -default_material_bed_temperature = =0 if extruders_enabled_count > 1 and (not support_enable or extruder_nr != support_extruder_nr) else 60 -machine_nozzle_cool_down_speed = 0.75 -machine_nozzle_heat_up_speed = 1.6 -prime_tower_enable = =min(extruderValues('material_surface_energy')) < 100 -speed_print = 80 -speed_topbottom = =math.ceil(speed_print * 30 / 80) -speed_wall = =math.ceil(speed_print * 40 / 80) -speed_wall_0 = =math.ceil(speed_wall * 30 / 40) -support_angle = 45 -support_bottom_distance = =math.ceil(min(extruderValues('material_adhesion_tendency')) / 2) * layer_height -support_infill_sparse_thickness = =2 * layer_height -support_interface_density = =min(extruderValues('material_surface_energy')) -support_interface_enable = True -support_top_distance = =math.ceil(min(extruderValues('material_adhesion_tendency')) / 1) * layer_height -top_bottom_thickness = 1 - diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.1mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.1mm.inst.cfg deleted file mode 100644 index 9972855717..0000000000 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.1mm.inst.cfg +++ /dev/null @@ -1,28 +0,0 @@ -[general] -definition = ultimaker_s8 -name = Fine -version = 4 - -[metadata] -material = generic_bam -quality_type = normal -setting_version = 25 -type = quality -variant = AA+ 0.4 -weight = 0 - -[values] -brim_replaces_support = False -build_volume_temperature = =50 if extruders_enabled_count > 1 and (not support_enable or extruder_nr != support_extruder_nr) else 24 -default_material_bed_temperature = =0 if extruders_enabled_count > 1 and (not support_enable or extruder_nr != support_extruder_nr) else 60 -machine_nozzle_cool_down_speed = 0.75 -machine_nozzle_heat_up_speed = 1.6 -prime_tower_enable = =min(extruderValues('material_surface_energy')) < 100 -support_angle = 45 -support_bottom_distance = =math.ceil(min(extruderValues('material_adhesion_tendency')) / 2) * layer_height -support_infill_sparse_thickness = =2 * layer_height -support_interface_density = =min(extruderValues('material_surface_energy')) -support_interface_enable = True -support_top_distance = =math.ceil(min(extruderValues('material_adhesion_tendency')) / 1) * layer_height -top_bottom_thickness = 1 - diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.2mm.inst.cfg deleted file mode 100644 index 8425b1842d..0000000000 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.2mm.inst.cfg +++ /dev/null @@ -1,31 +0,0 @@ -[general] -definition = ultimaker_s8 -name = Fast -version = 4 - -[metadata] -material = generic_bam -quality_type = draft -setting_version = 25 -type = quality -variant = AA+ 0.4 -weight = -2 - -[values] -brim_replaces_support = False -build_volume_temperature = =50 if extruders_enabled_count > 1 and (not support_enable or extruder_nr != support_extruder_nr) else 24 -default_material_bed_temperature = =0 if extruders_enabled_count > 1 and (not support_enable or extruder_nr != support_extruder_nr) else 60 -machine_nozzle_cool_down_speed = 0.75 -machine_nozzle_heat_up_speed = 1.6 -material_print_temperature = =default_material_print_temperature + 5 -prime_tower_enable = =min(extruderValues('material_surface_energy')) < 100 -speed_topbottom = =math.ceil(speed_print * 35 / 70) -speed_wall = =math.ceil(speed_print * 50 / 70) -speed_wall_0 = =math.ceil(speed_wall * 35 / 50) -support_angle = 45 -support_bottom_distance = =math.ceil(min(extruderValues('material_adhesion_tendency')) / 2) * layer_height -support_interface_density = =min(extruderValues('material_surface_energy')) -support_interface_enable = True -support_top_distance = =math.ceil(min(extruderValues('material_adhesion_tendency')) / 2) * layer_height -top_bottom_thickness = 1 - diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.3mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.3mm.inst.cfg deleted file mode 100644 index 69ad43b578..0000000000 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_bam_0.3mm.inst.cfg +++ /dev/null @@ -1,31 +0,0 @@ -[general] -definition = ultimaker_s8 -name = Extra Fast -version = 4 - -[metadata] -material = generic_bam -quality_type = verydraft -setting_version = 25 -type = quality -variant = AA+ 0.4 -weight = -3 - -[values] -brim_replaces_support = False -build_volume_temperature = =50 if extruders_enabled_count > 1 and (not support_enable or extruder_nr != support_extruder_nr) else 24 -default_material_bed_temperature = =0 if extruders_enabled_count > 1 and (not support_enable or extruder_nr != support_extruder_nr) else 60 -machine_nozzle_cool_down_speed = 0.75 -machine_nozzle_heat_up_speed = 1.6 -material_print_temperature = =default_material_print_temperature + 5 -prime_tower_enable = =min(extruderValues('material_surface_energy')) < 100 -speed_topbottom = =math.ceil(speed_print * 35 / 70) -speed_wall = =math.ceil(speed_print * 50 / 70) -speed_wall_0 = =math.ceil(speed_wall * 35 / 50) -support_angle = 45 -support_bottom_distance = 0.3 -support_interface_density = =min(extruderValues('material_surface_energy')) -support_interface_enable = True -support_top_distance = 0.3 -top_bottom_thickness = 1 - From c8491201ba5206391e9b28fec11a829f7fa93196 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Fri, 4 Jul 2025 12:17:38 +0000 Subject: [PATCH 101/159] Set conan package version 5.10.2 --- conandata.yml | 14 +++++++------- resources/conandata.yml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/conandata.yml b/conandata.yml index 17f8151a0e..9d709096f4 100644 --- a/conandata.yml +++ b/conandata.yml @@ -1,15 +1,15 @@ -version: "5.10.1" +version: "5.10.2" requirements: - - "cura_resources/5.10.1" - - "uranium/5.10.1" - - "curaengine/5.10.1" - - "cura_binary_data/5.10.1" - - "fdm_materials/5.10.1" + - "cura_resources/5.10.2" + - "uranium/5.10.2" + - "curaengine/5.10.2" + - "cura_binary_data/5.10.2" + - "fdm_materials/5.10.2" - "dulcificum/5.10.0" - "pysavitar/5.10.0" - "pynest2d/5.10.0" requirements_internal: - - "fdm_materials/5.10.1" + - "fdm_materials/5.10.2" - "cura_private_data/5.10.0-alpha.0@internal/testing" requirements_enterprise: - "native_cad_plugin/2.0.0" diff --git a/resources/conandata.yml b/resources/conandata.yml index 2b7075fa77..c4418d4f57 100644 --- a/resources/conandata.yml +++ b/resources/conandata.yml @@ -1 +1 @@ -version: "5.10.1" +version: "5.10.2" From 4aea5807cfe17dc8c0910be9d6e265e21b4d7644 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 4 Jul 2025 17:25:49 +0200 Subject: [PATCH 102/159] Fix SliceableObjectDecorator deep copy CURA-12543 --- cura/Scene/SliceableObjectDecorator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Scene/SliceableObjectDecorator.py b/cura/Scene/SliceableObjectDecorator.py index 4278705e2e..cc611b17af 100644 --- a/cura/Scene/SliceableObjectDecorator.py +++ b/cura/Scene/SliceableObjectDecorator.py @@ -60,6 +60,6 @@ class SliceableObjectDecorator(SceneNodeDecorator): def __deepcopy__(self, memo) -> "SliceableObjectDecorator": copied_decorator = SliceableObjectDecorator() - copied_decorator.setPaintTexture(copy.deepcopy(self.getPaintTexture())) + copied_decorator.setPaintTexture(copy.deepcopy(self.getPaintTexture(create_if_required = False))) copied_decorator.setTextureDataMapping(copy.deepcopy(self.getTextureDataMapping())) return copied_decorator From 2debe37e7236f42ab3457c94c7789103c70516de Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 7 Jul 2025 09:18:22 +0200 Subject: [PATCH 103/159] Use specific config folder for alpha versions CURA-12408 --- conandata.yml | 1 + conanfile.py | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/conandata.yml b/conandata.yml index 3181d601e5..b9a8b99eb1 100644 --- a/conandata.yml +++ b/conandata.yml @@ -1,4 +1,5 @@ version: "5.11.0-alpha.0" +commit: "unknown" requirements: - "cura_resources/5.11.0-alpha.0@ultimaker/testing" - "uranium/5.11.0-alpha.0@ultimaker/testing" diff --git a/conanfile.py b/conanfile.py index 28f45e7c24..652f151fc3 100644 --- a/conanfile.py +++ b/conanfile.py @@ -16,7 +16,7 @@ from conan import ConanFile from conan.tools.files import copy, rmdir, save, mkdir, rm, update_conandata from conan.tools.microsoft import unix_path from conan.tools.env import VirtualRunEnv, Environment, VirtualBuildEnv -from conan.tools.scm import Version +from conan.tools.scm import Version, Git from conan.errors import ConanInvalidConfiguration, ConanException required_conan_version = ">=2.7.0" # When changing the version, also change the one in conandata.yml/extra_dependencies @@ -329,10 +329,16 @@ class CuraConan(ConanFile): # If you want a specific Cura version to show up on the splash screen add the user configuration `user.cura:version=VERSION` # the global.conf, profile, package_info (of dependency) or via the cmd line `-c user.cura:version=VERSION` cura_version = Version(self.conf.get("user.cura:version", default = self.version, check_type = str)) - pre_tag = f"-{cura_version.pre}" if cura_version.pre else "" - build_tag = f"+{cura_version.build}" if cura_version.build else "" - internal_tag = f"+internal" if self.options.internal else "" - cura_version = f"{cura_version.major}.{cura_version.minor}.{cura_version.patch}{pre_tag}{build_tag}{internal_tag}" + extra_build_identifiers = [] + + if self.options.internal: + extra_build_identifiers.append("internal") + if cura_version.pre.startswith("alpha") and self.conan_data["commmit"] != "unknown": + extra_build_identifiers.append(self.conan_data["commmit"]) + + if extra_build_identifiers: + separator = "+" if not cura_version.build else "." + cura_version = Version(f"{cura_version}{separator}{".".join(extra_build_identifiers)}") self.output.info(f"Write CuraVersion.py to {self.recipe_folder}") @@ -340,7 +346,7 @@ class CuraConan(ConanFile): f.write(cura_version_py.render( cura_app_name = self.name, cura_app_display_name = self._app_name, - cura_version = cura_version, + cura_version = str(cura_version), cura_version_full = self.version, cura_build_type = "Enterprise" if self.options.enterprise else "", cura_debug_mode = self.options.cura_debug_mode, @@ -527,7 +533,7 @@ class CuraConan(ConanFile): )) def export(self): - update_conandata(self, {"version": self.version}) + update_conandata(self, {"version": self.version, "commit": Git(self).get_commit()}) def export_sources(self): copy(self, "*", os.path.join(self.recipe_folder, "plugins"), os.path.join(self.export_sources_folder, "plugins")) From 87eb3b47053959c4b70e004dc54afbe453cefaed Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 7 Jul 2025 09:25:39 +0200 Subject: [PATCH 104/159] Fix string comparison CURA-12408 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 652f151fc3..82f039f9b9 100644 --- a/conanfile.py +++ b/conanfile.py @@ -333,7 +333,7 @@ class CuraConan(ConanFile): if self.options.internal: extra_build_identifiers.append("internal") - if cura_version.pre.startswith("alpha") and self.conan_data["commmit"] != "unknown": + if str(cura_version.pre).startswith("alpha") and self.conan_data["commmit"] != "unknown": extra_build_identifiers.append(self.conan_data["commmit"]) if extra_build_identifiers: From b617f2c27e60e7b33f0fc307cb1b89290b2cd975 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 7 Jul 2025 09:29:30 +0200 Subject: [PATCH 105/159] Fix commit key (too many MMMMMs) CURA-12408 --- conanfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conanfile.py b/conanfile.py index 82f039f9b9..362861ae41 100644 --- a/conanfile.py +++ b/conanfile.py @@ -333,8 +333,8 @@ class CuraConan(ConanFile): if self.options.internal: extra_build_identifiers.append("internal") - if str(cura_version.pre).startswith("alpha") and self.conan_data["commmit"] != "unknown": - extra_build_identifiers.append(self.conan_data["commmit"]) + if str(cura_version.pre).startswith("alpha") and self.conan_data["commit"] != "unknown": + extra_build_identifiers.append(self.conan_data["commit"]) if extra_build_identifiers: separator = "+" if not cura_version.build else "." From 581d8e3a1299a0b75ba61332c019649c9e07fd0c Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 7 Jul 2025 09:55:37 +0200 Subject: [PATCH 106/159] Use shorter commit reference CURA-12408 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 362861ae41..89b2da35d6 100644 --- a/conanfile.py +++ b/conanfile.py @@ -334,7 +334,7 @@ class CuraConan(ConanFile): if self.options.internal: extra_build_identifiers.append("internal") if str(cura_version.pre).startswith("alpha") and self.conan_data["commit"] != "unknown": - extra_build_identifiers.append(self.conan_data["commit"]) + extra_build_identifiers.append(self.conan_data["commit"][:6]) if extra_build_identifiers: separator = "+" if not cura_version.build else "." From 55ee4ec6e1c32f57e64f719e630cefa86e01559a Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 8 Jul 2025 15:47:46 +0200 Subject: [PATCH 107/159] Calculate and apply UV coordinates CURA-12528 --- cura/Scene/SliceableObjectDecorator.py | 22 ++++++++++------------ plugins/3MFReader/ThreeMFReader.py | 2 -- plugins/PaintTool/PaintTool.qml | 14 ++++++++++---- plugins/PaintTool/PaintView.py | 8 ++++++++ 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/cura/Scene/SliceableObjectDecorator.py b/cura/Scene/SliceableObjectDecorator.py index cc611b17af..7ee77795e7 100644 --- a/cura/Scene/SliceableObjectDecorator.py +++ b/cura/Scene/SliceableObjectDecorator.py @@ -12,10 +12,6 @@ from UM.View.GL.OpenGL import OpenGL from UM.View.GL.Texture import Texture -# FIXME: When the texture UV-unwrapping is done, these two values will need to be set to a proper value (suggest 4096 for both). -TEXTURE_WIDTH = 512 -TEXTURE_HEIGHT = 512 - class SliceableObjectDecorator(SceneNodeDecorator): def __init__(self) -> None: super().__init__() @@ -25,15 +21,10 @@ class SliceableObjectDecorator(SceneNodeDecorator): def isSliceable(self) -> bool: return True - def getPaintTexture(self, create_if_required: bool = True) -> Optional[UM.View.GL.Texture.Texture]: - if self._paint_texture is None and create_if_required: - self._paint_texture = OpenGL.getInstance().createTexture(TEXTURE_WIDTH, TEXTURE_HEIGHT) - image = QImage(TEXTURE_WIDTH, TEXTURE_HEIGHT, QImage.Format.Format_RGB32) - image.fill(0) - self._paint_texture.setImage(image) + def getPaintTexture(self) -> Optional[Texture]: return self._paint_texture - def setPaintTexture(self, texture: UM.View.GL.Texture) -> None: + def setPaintTexture(self, texture: Texture) -> None: self._paint_texture = texture def getTextureDataMapping(self) -> Dict[str, tuple[int, int]]: @@ -42,6 +33,13 @@ class SliceableObjectDecorator(SceneNodeDecorator): def setTextureDataMapping(self, mapping: Dict[str, tuple[int, int]]) -> None: self._texture_data_mapping = mapping + def prepareTexture(self, width: int, height: int) -> None: + if self._paint_texture is None: + self._paint_texture = OpenGL.getInstance().createTexture(width, height) + image = QImage(width, height, QImage.Format.Format_RGB32) + image.fill(0) + self._paint_texture.setImage(image) + def packTexture(self) -> Optional[bytearray]: if self._paint_texture is None: return None @@ -60,6 +58,6 @@ class SliceableObjectDecorator(SceneNodeDecorator): def __deepcopy__(self, memo) -> "SliceableObjectDecorator": copied_decorator = SliceableObjectDecorator() - copied_decorator.setPaintTexture(copy.deepcopy(self.getPaintTexture(create_if_required = False))) + copied_decorator.setPaintTexture(copy.deepcopy(self.getPaintTexture())) copied_decorator.setTextureDataMapping(copy.deepcopy(self.getTextureDataMapping())) return copied_decorator diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index 730acf4af6..09143dde64 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -153,8 +153,6 @@ class ThreeMFReader(MeshReader): # It is only set for the root node of the 3mf file mesh_builder.setFileName(file_name) - mesh_builder.unwrapNewUvs() - mesh_data = mesh_builder.build() if len(mesh_data.getVertices()): diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml index 4cbe9d4ade..ef1ac35628 100644 --- a/plugins/PaintTool/PaintTool.qml +++ b/plugins/PaintTool/PaintTool.qml @@ -164,17 +164,22 @@ Item width: parent.width indicatorVisible: false - from: 1 - to: 40 - value: 10 + from: 10 + to: 1000 + value: 200 onPressedChanged: function(pressed) { if(! pressed) { - UM.Controller.triggerActionWithData("setBrushSize", shapeSizeSlider.value) + setBrushSize() } } + + function setBrushSize() + { + UM.Controller.triggerActionWithData("setBrushSize", shapeSizeSlider.value) + } } //Line between the sections. @@ -233,5 +238,6 @@ Item rowPaintMode.children[0].setMode() rowBrushColor.children[1].setColor() rowBrushShape.children[1].setShape() + shapeSizeSlider.setBrushSize() } } diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index 22eb8c55f6..2b0181be70 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -147,6 +147,14 @@ class PaintView(View): paint_data_mapping[paint_type] = new_mapping node.callDecoration("setTextureDataMapping", paint_data_mapping) + mesh = node.getMeshData() + if not mesh.hasUVCoordinates(): + texture_width, texture_height = mesh.calculateUnwrappedUVCoordinates(4096) + 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] From 20a2664294d0028ba4924bfe02186fbbacd1f18d Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 14 Jul 2025 13:36:48 +0200 Subject: [PATCH 108/159] Remove the desired texture definition CURA-12528 --- plugins/PaintTool/PaintView.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index 2b0181be70..c2562fa36a 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -149,7 +149,7 @@ class PaintView(View): mesh = node.getMeshData() if not mesh.hasUVCoordinates(): - texture_width, texture_height = mesh.calculateUnwrappedUVCoordinates(4096) + texture_width, texture_height = mesh.calculateUnwrappedUVCoordinates() node.callDecoration("prepareTexture", texture_width, texture_height) if hasattr(mesh, OpenGL.VertexBufferProperty): # Force clear OpenGL buffer so that new UV coordinates will be sent From b3552d8b20c66c7d0b51c5f2ca8567a8e171fd53 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 16 Jul 2025 11:27:49 +0200 Subject: [PATCH 109/159] Basic warning on unused extruder. Since the bed-temp (for example) might be unfluenced by unused but (probably accidentally) enabled extruders, we want to be able to warn the user of such. part of CURA-12622 --- .../CuraEngineBackend/CuraEngineBackend.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index e3e15f5381..eb65cafb68 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -2,6 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. import argparse #To run the engine in debug mode if the front-end is in debug mode. +from cmath import isnan from collections import defaultdict import os from PyQt6.QtCore import QObject, QTimer, QUrl, pyqtSlot @@ -960,8 +961,26 @@ class CuraEngineBackend(QObject, Backend): """ material_amounts = [] + no_use_warnings = [] for index in range(message.repeatedMessageCount("materialEstimates")): - material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount) + material_use_for_tool = message.getRepeatedMessage("materialEstimates", index).material_amount + if isnan(material_use_for_tool): + material_amounts.append(0.0) + if self._global_container_stack.extruderList[int(index)].isEnabled: + no_use_warnings.append(index) + else: + material_amounts.append(material_use_for_tool) + + if no_use_warnings: + extruder_names = [self._global_container_stack.extruderList[int(idx)].definition.getName() for idx in no_use_warnings] + Message( + text=catalog.i18nc("@message", "At least one extruder that wasn't disabled, remains unused in this print (preview). " + f"Unused extruders: '{", ".join(extruder_names)}'. This can sometimes become a problem " + "(for example when the bed-temperature is adjusted by the material-profile present in the unused extruder). " + "It therefore might be desirable to disable these unused extruders manually, depending on the situation. "), + title=catalog.i18nc("@message:title", "Unused Non-Disabled Extruder"), + message_type=Message.MessageType.WARNING + ).show() times = self._parseMessagePrintTimes(message) self.printDurationMessage.emit(self._start_slice_job_build_plate, times, material_amounts) From b1676c66fdc7ceea2a82e964219c1059fdced9bf Mon Sep 17 00:00:00 2001 From: HellAholic Date: Thu, 17 Jul 2025 09:15:00 +0200 Subject: [PATCH 110/159] Add run-name Identify the build's origin by looking at the workflow --- .github/workflows/find-packages.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/find-packages.yml b/.github/workflows/find-packages.yml index 81f68857e5..788ec4021d 100644 --- a/.github/workflows/find-packages.yml +++ b/.github/workflows/find-packages.yml @@ -1,4 +1,5 @@ name: Find packages for Jira ticket and create installers +run-name: ${{ inputs.jira_ticket_number }} by @${{ github.actor }} on: workflow_dispatch: From 2c0dc004f3a71c5d56a2dae546b15a3cdbffc292 Mon Sep 17 00:00:00 2001 From: Paul Kuiper <46715907+pkuiper-ultimaker@users.noreply.github.com> Date: Fri, 18 Jul 2025 19:22:00 +0200 Subject: [PATCH 111/159] - New CC+0.6 variant - All CC+0.4 print modes experimental - Support PC, CPE+ in AA+0.4 core - Improve surface quality of PET-CF and PA-CF profiles - Added back the 0.3mm profiles for CC+0.6 - Small bug fix: remove self reference to s8 quality profiles (enables the return of BASF metal profiles) --- resources/definitions/ultimaker_s8.def.json | 3 +- ..._cc_plus_0.6_nylon-cf-slide_0.2mm.inst.cfg | 38 ++++++++++- ..._cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg | 61 +++++++++++++++++ .../um_s8_cc_plus_0.6_petcf_0.2mm.inst.cfg | 38 ++++++++++- .../um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg | 65 +++++++++++++++++++ 5 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg diff --git a/resources/definitions/ultimaker_s8.def.json b/resources/definitions/ultimaker_s8.def.json index fb8243cd60..b7de27722d 100644 --- a/resources/definitions/ultimaker_s8.def.json +++ b/resources/definitions/ultimaker_s8.def.json @@ -1,7 +1,7 @@ { "version": 2, "name": "UltiMaker S8", - "inherits": "ultimaker_s7", + "inherits": "ultimaker_s5", "metadata": { "visible": true, @@ -48,7 +48,6 @@ "preferred_material": "ultimaker_pla_blue", "preferred_quality_type": "draft", "preferred_variant_name": "AA+ 0.4", - "quality_definition": "ultimaker_s8", "supported_actions": [ "DiscoverUM3Action" ], "supports_material_export": true, "supports_network_connection": true, diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm.inst.cfg index 5741714558..eb29199252 100644 --- a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm.inst.cfg @@ -12,15 +12,49 @@ variant = CC+ 0.6 weight = -2 [values] +acceleration_roofing = =acceleration_topbottom/2 +bridge_enable_more_layers = True +bridge_skin_density = 70 bridge_skin_material_flow = 100 +bridge_skin_material_flow_2 = 70 bridge_skin_speed = 30 +bridge_skin_speed_2 = =speed_print*2/3 bridge_wall_material_flow = 100 +bridge_wall_min_length = 2 bridge_wall_speed = 30 +cool_min_layer_time = 6 cool_min_layer_time_fan_speed_max = 11 -retraction_prime_speed = 15 -speed_print = 85 +cool_min_layer_time_overhang = 11 +cool_min_temperature = =material_print_temperature-10 +flooring_monotonic = False +infill_material_flow = =material_flow if infill_sparse_density < 95 else 95 +infill_pattern = ='zigzag' if infill_sparse_density > 50 else 'grid' +jerk_roofing = =jerk_print +material_flow = 95 +retraction_hop_enabled = False +retraction_prime_speed = 25 +roofing_line_width = 0.5 +roofing_material_flow = =skin_material_flow +roofing_monotonic = False +skin_material_flow = =0.95*material_flow +skin_outline_count = 0 +speed_print = 80 +speed_roofing = 50 speed_wall = =speed_print +speed_wall_0_roofing = =speed_roofing +speed_wall_x_roofing = =speed_roofing +support_bottom_distance = =support_z_distance support_line_width = 0.6 support_structure = tree +support_tree_tip_diameter = 2.0 +support_tree_top_rate = 10 +support_xy_distance = 1.2 +support_xy_distance_overhang = =1.5*machine_nozzle_size +support_z_distance = =min(2*layer_height, 0.4) +top_bottom_thickness = =wall_thickness +wall_0_inset = =0.05 +wall_line_width_0 = 0.5 wall_overhang_speed_factors = [100,90,80,70,60,50] +wall_x_material_flow = =material_flow +xy_offset = 0.075 diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg new file mode 100644 index 0000000000..ed86b8e735 --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg @@ -0,0 +1,61 @@ +[general] +definition = ultimaker_s8 +name = Extra Fast +version = 4 + +[metadata] +material = generic_nylon-cf-slide +quality_type = verydraft +setting_version = 25 +type = quality +variant = CC+ 0.6 +weight = -3 + +[values] +acceleration_roofing = =acceleration_topbottom/2 +bridge_enable_more_layers = True +bridge_skin_density = 70 +bridge_skin_material_flow = 100 +bridge_skin_material_flow_2 = 70 +bridge_skin_speed = 30 +bridge_skin_speed_2 = =speed_print*2/3 +bridge_wall_material_flow = 100 +bridge_wall_min_length = 2 +bridge_wall_speed = 30 +cool_min_layer_time = 6 +cool_min_layer_time_fan_speed_max = 11 +cool_min_layer_time_overhang = 11 +cool_min_temperature = =material_print_temperature-10 +flooring_monotonic = False +infill_material_flow = =material_flow if infill_sparse_density < 95 else 95 +infill_pattern = ='zigzag' if infill_sparse_density > 50 else 'grid' +jerk_roofing = =jerk_print +material_flow = 95 +retraction_hop_enabled = False +retraction_prime_speed = 25 +roofing_line_width = 0.5 +roofing_material_flow = =skin_material_flow +roofing_monotonic = False +skin_material_flow = =0.95*material_flow +skin_outline_count = 0 +speed_print = 55 +speed_roofing = 50 +speed_support_interface = 55 +speed_wall = =speed_print +speed_wall_0_roofing = =speed_roofing +speed_wall_x_roofing = =speed_roofing +support_bottom_distance = =support_z_distance +support_line_width = 0.6 +support_structure = tree +support_tree_tip_diameter = 2.0 +support_tree_top_rate = 10 +support_xy_distance = 1.2 +support_xy_distance_overhang = =1.5*machine_nozzle_size +support_z_distance = =min(2*layer_height, 0.4) +top_bottom_thickness = =wall_thickness +wall_0_inset = =0.05 +wall_line_width_0 = 0.5 +wall_overhang_speed_factors = [100,90,80,70,60,50] +wall_x_material_flow = =material_flow +xy_offset = 0.075 + diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm.inst.cfg index 803717ba02..b568f01cde 100644 --- a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm.inst.cfg @@ -12,16 +12,52 @@ variant = CC+ 0.6 weight = -2 [values] +acceleration_roofing = =acceleration_topbottom/2 adhesion_type = skirt +bridge_enable_more_layers = True +bridge_skin_density = 70 bridge_skin_material_flow = 100 +bridge_skin_material_flow_2 = 70 bridge_skin_speed = 30 +bridge_skin_speed_2 = =speed_print*2/3 bridge_wall_material_flow = 100 +bridge_wall_min_length = 2 bridge_wall_speed = 30 -speed_print = 85 +cool_min_layer_time = 6 +cool_min_layer_time_overhang = 11 +cool_min_temperature = =material_print_temperature-10 +flooring_monotonic = False +infill_material_flow = =material_flow if infill_sparse_density < 95 else 95 +infill_pattern = ='zigzag' if infill_sparse_density > 50 else 'grid' +jerk_roofing = =jerk_print +material_pressure_advance_factor = 0.25 +retraction_hop_enabled = False +retraction_prime_speed = 15 +roofing_line_width = 0.5 +roofing_material_flow = =skin_material_flow +roofing_monotonic = False +skin_material_flow = =0.95*material_flow +skin_outline_count = 0 +skirt_height = 5 +speed_print = 80 +speed_roofing = 50 speed_wall = =speed_print +speed_wall_0_roofing = =speed_roofing +speed_wall_x_roofing = =speed_roofing +support_bottom_distance = =support_z_distance support_interface_enable = False support_line_width = 0.6 support_structure = tree +support_tree_tip_diameter = 2.0 +support_tree_top_rate = 10 +support_xy_distance = 1.2 +support_xy_distance_overhang = =1.5*machine_nozzle_size +support_z_distance = =min(2*layer_height, 0.4) switch_extruder_retraction_amount = 16 +top_bottom_thickness = =wall_thickness +wall_0_inset = =0.05 +wall_line_width_0 = 0.5 wall_overhang_speed_factors = [100,90,80,70,60,50] +wall_x_material_flow = =material_flow +xy_offset = 0.075 diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg new file mode 100644 index 0000000000..9f5a1add1f --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg @@ -0,0 +1,65 @@ +[general] +definition = ultimaker_s8 +name = Extra Fast +version = 4 + +[metadata] +material = generic_petcf +quality_type = verydraft +setting_version = 25 +type = quality +variant = CC+ 0.6 +weight = -3 + +[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 +material_print_temperature = =default_material_print_temperature + 10 +retraction_hop_enabled = False +retraction_prime_speed = 15 +roofing_line_width = 0.5 +roofing_material_flow = =skin_material_flow +roofing_monotonic = False +skin_material_flow = =0.95*material_flow +skin_outline_count = 0 +skirt_height = 5 +speed_print = 55 +speed_roofing = 50 +speed_support_interface = 55 +speed_wall = =speed_print +speed_wall_0_roofing = =speed_roofing +speed_wall_x_roofing = =speed_roofing +support_bottom_distance = =support_z_distance +support_interface_enable = False +support_line_width = 0.6 +support_structure = tree +support_tree_tip_diameter = 2.0 +support_tree_top_rate = 10 +support_xy_distance = 1.2 +support_xy_distance_overhang = =1.5*machine_nozzle_size +support_z_distance = =min(2*layer_height, 0.4) +switch_extruder_retraction_amount = 16 +top_bottom_thickness = =wall_thickness +wall_0_inset = =0.05 +wall_line_width_0 = =line_width * (1 + magic_spiralize * 0.25) +wall_overhang_speed_factors = [100,90,80,70,60,50] +wall_x_material_flow = =material_flow +xy_offset = 0.075 + From c7b86ae1c43480c946fe4bd2885ec0e2db5f2d7f Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 21 Jul 2025 12:00:05 +0200 Subject: [PATCH 112/159] Refine automagic installers workflow --- .github/workflows/find-packages.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/find-packages.yml b/.github/workflows/find-packages.yml index 788ec4021d..93a5bdde2b 100644 --- a/.github/workflows/find-packages.yml +++ b/.github/workflows/find-packages.yml @@ -1,15 +1,16 @@ -name: Find packages for Jira ticket and create installers +name: All installers (based on Jira ticket) run-name: ${{ inputs.jira_ticket_number }} by @${{ github.actor }} on: workflow_dispatch: inputs: jira_ticket_number: - description: 'Jira ticket number for Conan package discovery (e.g., cura_12345)' + description: 'Jira ticket number (e.g. CURA-15432 or cura_12345)' required: true type: string start_builds: - default: false + description: 'Start installers build based on found packages' + default: true required: false type: boolean conan_args: From a1ff74f5397dbd5f28f4222d4e5bbab23cbcf4a2 Mon Sep 17 00:00:00 2001 From: Frederic Meeuwissen <13856291+Frederic98@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:21:12 +0200 Subject: [PATCH 113/159] [PP-639] Move some setting overrides from S7 to S5 --- resources/definitions/ultimaker_s5.def.json | 7 ++++++- resources/definitions/ultimaker_s7.def.json | 4 +--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/resources/definitions/ultimaker_s5.def.json b/resources/definitions/ultimaker_s5.def.json index 46d46d33eb..fa6f5a9a38 100644 --- a/resources/definitions/ultimaker_s5.def.json +++ b/resources/definitions/ultimaker_s5.def.json @@ -72,7 +72,11 @@ "brim_width": { "value": "3" }, "build_volume_temperature": { "maximum_value": 50 }, "cool_fan_speed": { "value": "50" }, - "default_material_print_temperature": { "value": "200" }, + "default_material_print_temperature": + { + "maximum_value_warning": "320", + "value": "200" + }, "extruder_prime_pos_abs": { "default_value": true }, "gantry_height": { "value": "55" }, "infill_pattern": { "value": "'zigzag' if infill_sparse_density > 80 else 'triangles'" }, @@ -104,6 +108,7 @@ "machine_nozzle_heat_up_speed": { "default_value": 1.4 }, "machine_start_gcode": { "default_value": "" }, "machine_width": { "default_value": 330 }, + "material_print_temperature_layer_0": { "maximum_value_warning": "320" }, "multiple_mesh_overlap": { "value": "0" }, "optimize_wall_printing_order": { "value": "True" }, "prime_blob_enable": diff --git a/resources/definitions/ultimaker_s7.def.json b/resources/definitions/ultimaker_s7.def.json index 14d9b21168..ac2f927ad4 100644 --- a/resources/definitions/ultimaker_s7.def.json +++ b/resources/definitions/ultimaker_s7.def.json @@ -46,8 +46,6 @@ }, "overrides": { - "default_material_print_temperature": { "maximum_value_warning": "320" }, - "machine_name": { "default_value": "Ultimaker S7" }, - "material_print_temperature_layer_0": { "maximum_value_warning": "320" } + "machine_name": { "default_value": "Ultimaker S7" } } } \ No newline at end of file From 5fbf6af420e4978aff66a9661a3b9f84e9422078 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 22 Jul 2025 15:48:33 +0200 Subject: [PATCH 114/159] Do not use default xz compression Because it is not supported by the appimage toolkit --- packaging/AppImage-builder/AppImageBuilder.yml.jinja | 1 + 1 file changed, 1 insertion(+) diff --git a/packaging/AppImage-builder/AppImageBuilder.yml.jinja b/packaging/AppImage-builder/AppImageBuilder.yml.jinja index c6e7a7123a..fe191fdc54 100644 --- a/packaging/AppImage-builder/AppImageBuilder.yml.jinja +++ b/packaging/AppImage-builder/AppImageBuilder.yml.jinja @@ -77,3 +77,4 @@ AppImage: arch: {{ arch }} file_name: {{ file_name }} update-information: guess + comp: zlib From 51ff682556498d754526676c6252d03db0a84065 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 22 Jul 2025 15:59:13 +0200 Subject: [PATCH 115/159] Give the proper compressor name --- packaging/AppImage-builder/AppImageBuilder.yml.jinja | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/AppImage-builder/AppImageBuilder.yml.jinja b/packaging/AppImage-builder/AppImageBuilder.yml.jinja index fe191fdc54..29144b0e77 100644 --- a/packaging/AppImage-builder/AppImageBuilder.yml.jinja +++ b/packaging/AppImage-builder/AppImageBuilder.yml.jinja @@ -77,4 +77,4 @@ AppImage: arch: {{ arch }} file_name: {{ file_name }} update-information: guess - comp: zlib + comp: gzip From d61c87b9fe5012007ab99ba5ae2d1a6dba437426 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 22 Jul 2025 16:13:21 +0200 Subject: [PATCH 116/159] Try more efficient compression ratio --- packaging/AppImage-builder/AppImageBuilder.yml.jinja | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/AppImage-builder/AppImageBuilder.yml.jinja b/packaging/AppImage-builder/AppImageBuilder.yml.jinja index 29144b0e77..601189bb28 100644 --- a/packaging/AppImage-builder/AppImageBuilder.yml.jinja +++ b/packaging/AppImage-builder/AppImageBuilder.yml.jinja @@ -77,4 +77,4 @@ AppImage: arch: {{ arch }} file_name: {{ file_name }} update-information: guess - comp: gzip + comp: lzma From ef116edafe38a9d079cc33d447d8dccaa928ba24 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 22 Jul 2025 16:35:34 +0200 Subject: [PATCH 117/159] Revert "Try more efficient compression ratio" This reverts commit d61c87b9fe5012007ab99ba5ae2d1a6dba437426. --- packaging/AppImage-builder/AppImageBuilder.yml.jinja | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/AppImage-builder/AppImageBuilder.yml.jinja b/packaging/AppImage-builder/AppImageBuilder.yml.jinja index 601189bb28..29144b0e77 100644 --- a/packaging/AppImage-builder/AppImageBuilder.yml.jinja +++ b/packaging/AppImage-builder/AppImageBuilder.yml.jinja @@ -77,4 +77,4 @@ AppImage: arch: {{ arch }} file_name: {{ file_name }} update-information: guess - comp: lzma + comp: gzip From a39f6c94fa0f139ecb5fa50e1d2e9bee3d3865e0 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 24 Jul 2025 10:50:31 +0200 Subject: [PATCH 118/159] Make the warning message more concise CURA-12622 The purpose being to make the message as short as possible so that people are not too much discouraged to read it --- plugins/CuraEngineBackend/CuraEngineBackend.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index eb65cafb68..bea5de1bce 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -973,12 +973,13 @@ class CuraEngineBackend(QObject, Backend): if no_use_warnings: extruder_names = [self._global_container_stack.extruderList[int(idx)].definition.getName() for idx in no_use_warnings] + unused_extruders = [f"

  • {extruder_name}
  • " for extruder_name in extruder_names] Message( - text=catalog.i18nc("@message", "At least one extruder that wasn't disabled, remains unused in this print (preview). " - f"Unused extruders: '{", ".join(extruder_names)}'. This can sometimes become a problem " - "(for example when the bed-temperature is adjusted by the material-profile present in the unused extruder). " - "It therefore might be desirable to disable these unused extruders manually, depending on the situation. "), - title=catalog.i18nc("@message:title", "Unused Non-Disabled Extruder"), + text=catalog.i18nc("@message", "At least one extruder remains unused in this print:" + f"
      {"".join(unused_extruders)}

    This can sometimes become a problem, " + "for example when the bed-temperature is adjusted by the material-profile present in the unused extruder. " + "It might be desirable to disable these unused extruders."), + title=catalog.i18nc("@message:title", "Unused Extruder"), message_type=Message.MessageType.WARNING ).show() From e8423755a48f628b841f9701300adf24716e33d3 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 24 Jul 2025 11:14:36 +0200 Subject: [PATCH 119/159] Add button to auto-disable unused extruders CURA-12622 --- .../CuraEngineBackend/CuraEngineBackend.py | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index bea5de1bce..f8736d69b8 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -159,6 +159,7 @@ class CuraEngineBackend(QObject, Backend): self._backend_log_max_lines: int = 20000 # Maximum number of lines to buffer self._error_message: Optional[Message] = None # Pop-up message that shows errors. + self._unused_extruders: list[int] = [] # Extruder numbers of found unused extruders # Count number of objects to see if there is something changed self._last_num_objects: Dict[int, int] = defaultdict(int) @@ -961,31 +962,44 @@ class CuraEngineBackend(QObject, Backend): """ material_amounts = [] - no_use_warnings = [] + self._unused_extruders = [] for index in range(message.repeatedMessageCount("materialEstimates")): material_use_for_tool = message.getRepeatedMessage("materialEstimates", index).material_amount if isnan(material_use_for_tool): material_amounts.append(0.0) if self._global_container_stack.extruderList[int(index)].isEnabled: - no_use_warnings.append(index) + self._unused_extruders.append(index) else: material_amounts.append(material_use_for_tool) - if no_use_warnings: - extruder_names = [self._global_container_stack.extruderList[int(idx)].definition.getName() for idx in no_use_warnings] + if self._unused_extruders: + extruder_names = [self._global_container_stack.extruderList[int(idx)].definition.getName() for idx in self._unused_extruders] unused_extruders = [f"
  • {extruder_name}
  • " for extruder_name in extruder_names] - Message( + warning_message = Message( text=catalog.i18nc("@message", "At least one extruder remains unused in this print:" f"
      {"".join(unused_extruders)}

    This can sometimes become a problem, " - "for example when the bed-temperature is adjusted by the material-profile present in the unused extruder. " + "for example when the bed temperature is adjusted for the material present in the unused extruder. " "It might be desirable to disable these unused extruders."), - title=catalog.i18nc("@message:title", "Unused Extruder"), + title=catalog.i18nc("@message:title", "Unused Extruder(s)"), message_type=Message.MessageType.WARNING - ).show() + ) + warning_message.addAction("disable_extruders", + name=catalog.i18nc("@button", "Disable unused extruder(s)"), + icon="", + description=catalog.i18nc("@label", "Automatically disable the unused extruder(s)") + ) + warning_message.actionTriggered.connect(self._onMessageActionTriggered) + warning_message.show() times = self._parseMessagePrintTimes(message) self.printDurationMessage.emit(self._start_slice_job_build_plate, times, material_amounts) + def _onMessageActionTriggered(self, message: Message, message_action: str) -> None: + if message_action == "disable_extruders": + message.hide() + for unused_extruder in self._unused_extruders: + CuraApplication.getInstance().getMachineManager().setExtruderEnabled(unused_extruder, False) + def _parseMessagePrintTimes(self, message: Arcus.PythonMessage) -> Dict[str, float]: """Called for parsing message to retrieve estimated time per feature From 890543b7de78a5a6f879c14d159b77482b287d13 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 25 Jul 2025 11:02:37 +0200 Subject: [PATCH 120/159] Change Marketplace dialog creation and destruction CURA-11810 This will hopefully fix some display issues we have, especially on Mac platforms --- plugins/Marketplace/Marketplace.py | 29 +- .../Marketplace/resources/qml/Marketplace.qml | 440 +++++++++--------- 2 files changed, 229 insertions(+), 240 deletions(-) diff --git a/plugins/Marketplace/Marketplace.py b/plugins/Marketplace/Marketplace.py index 86910f8f4a..fc287b5877 100644 --- a/plugins/Marketplace/Marketplace.py +++ b/plugins/Marketplace/Marketplace.py @@ -21,7 +21,6 @@ class Marketplace(Extension, QObject): def __init__(self, parent: Optional[QObject] = None) -> None: QObject.__init__(self, parent) Extension.__init__(self) - self._window: Optional["QObject"] = None # If the window has been loaded yet, it'll be cached in here. self._package_manager = CuraApplication.getInstance().getPackageManager() self._material_package_list: Optional[RemotePackageList] = None @@ -79,20 +78,17 @@ class Marketplace(Extension, QObject): If the window hadn't been loaded yet into Qt, it will be created lazily. """ - if self._window is None: - plugin_registry = PluginRegistry.getInstance() - plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.checkIfRestartNeeded) - plugin_path = plugin_registry.getPluginPath(self.getPluginId()) - if plugin_path is None: - plugin_path = os.path.dirname(__file__) - path = os.path.join(plugin_path, "resources", "qml", "Marketplace.qml") - self._window = CuraApplication.getInstance().createQmlComponent(path, {"manager": self}) - if self._window is None: # Still None? Failed to load the QML then. - return - if not self._window.isVisible(): - self.setTabShown(0) - self._window.show() - self._window.requestActivate() # Bring window into focus, if it was already open in the background. + + plugin_registry = PluginRegistry.getInstance() + plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.checkIfRestartNeeded) + plugin_path = plugin_registry.getPluginPath(self.getPluginId()) + if plugin_path is None: + plugin_path = os.path.dirname(__file__) + path = os.path.join(plugin_path, "resources", "qml", "Marketplace.qml") + window = CuraApplication.getInstance().createQmlSubWindow(path, {"manager": self}) + + if window is not None: # Still None? Failed to load the QML then. + window.show() @pyqtSlot() def setVisibleTabToMaterials(self) -> None: @@ -103,9 +99,6 @@ class Marketplace(Extension, QObject): self.setTabShown(1) def checkIfRestartNeeded(self) -> None: - if self._window is None: - return - if self._package_manager.hasPackagesToRemoveOrInstall or \ PluginRegistry.getInstance().getCurrentSessionActivationChangedPlugins(): self._restart_needed = True diff --git a/plugins/Marketplace/resources/qml/Marketplace.qml b/plugins/Marketplace/resources/qml/Marketplace.qml index 8028b89e02..c858297ac9 100644 --- a/plugins/Marketplace/resources/qml/Marketplace.qml +++ b/plugins/Marketplace/resources/qml/Marketplace.qml @@ -9,7 +9,7 @@ import QtQuick.Window 2.2 import UM 1.5 as UM import Cura 1.6 as Cura -Window +UM.Dialog { id: marketplaceDialog property variant catalog: UM.I18nCatalog { name: "cura" } @@ -25,293 +25,289 @@ Window width: minimumWidth height: minimumHeight - onVisibleChanged: - { - while(contextStack.depth > 1) - { - contextStack.pop(); //Do NOT use the StackView.Immediate transition here, since it causes the window to stay empty. Seemingly a Qt bug: https://bugreports.qt.io/browse/QTBUG-60670? - } - } - - Connections - { - target: Cura.API.account - function onLoginStateChanged() - { - close(); - } - } - title: "Marketplace" //Seen by Ultimaker as a brand name, so this doesn't get translated. - modality: Qt.NonModal // Background color Rectangle { anchors.fill: parent color: UM.Theme.getColor("main_background") - } - //The Marketplace can have a page in front of everything with package details. The stack view controls its visibility. - StackView - { - id: contextStack - anchors.fill: parent - initialItem: packageBrowse - - ColumnLayout + //The Marketplace can have a page in front of everything with package details. The stack view controls its visibility. + StackView { - id: packageBrowse + id: contextStack + anchors.fill: parent - spacing: UM.Theme.getSize("narrow_margin").height + initialItem: packageBrowse - // Page title. - Item + ColumnLayout { - Layout.preferredWidth: parent.width - Layout.preferredHeight: childrenRect.height + UM.Theme.getSize("default_margin").height + id: packageBrowse - UM.Label + spacing: UM.Theme.getSize("narrow_margin").height + + // Page title. + Item { - id: pageTitle - anchors - { - left: parent.left - leftMargin: UM.Theme.getSize("default_margin").width - right: parent.right - rightMargin: UM.Theme.getSize("default_margin").width - bottom: parent.bottom - } + Layout.preferredWidth: parent.width + Layout.preferredHeight: childrenRect.height + UM.Theme.getSize("default_margin").height - font: UM.Theme.getFont("large") - text: content.item ? content.item.pageTitle: catalog.i18nc("@title", "Loading...") + UM.Label + { + id: pageTitle + anchors + { + left: parent.left + leftMargin: UM.Theme.getSize("default_margin").width + right: parent.right + rightMargin: UM.Theme.getSize("default_margin").width + bottom: parent.bottom + } + + font: UM.Theme.getFont("large") + text: content.item ? content.item.pageTitle : catalog.i18nc("@title", "Loading...") + } } - } - OnboardBanner - { - id: onBoardBanner - visible: content.item && content.item.bannerVisible - text: content.item && content.item.bannerText - icon: content.item && content.item.bannerIcon - onRemove: content.item && content.item.onRemoveBanner - readMoreUrl: content.item && content.item.bannerReadMoreUrl - - Layout.fillWidth: true - Layout.leftMargin: UM.Theme.getSize("default_margin").width - Layout.rightMargin: UM.Theme.getSize("default_margin").width - } - - // Search & Top-Level Tabs - Item - { - id: searchHeader - implicitHeight: childrenRect.height - implicitWidth: parent.width - 2 * UM.Theme.getSize("default_margin").width - Layout.alignment: Qt.AlignHCenter - RowLayout + OnboardBanner { - width: parent.width - height: UM.Theme.getSize("button_icon").height + UM.Theme.getSize("default_margin").height - spacing: UM.Theme.getSize("thin_margin").width + id: onBoardBanner + visible: content.item && content.item.bannerVisible + text: content.item && content.item.bannerText + icon: content.item && content.item.bannerIcon + onRemove: content.item && content.item.onRemoveBanner + readMoreUrl: content.item && content.item.bannerReadMoreUrl - Cura.SearchBar + Layout.fillWidth: true + Layout.leftMargin: UM.Theme.getSize("default_margin").width + Layout.rightMargin: UM.Theme.getSize("default_margin").width + } + + // Search & Top-Level Tabs + Item + { + id: searchHeader + implicitHeight: childrenRect.height + implicitWidth: parent.width - 2 * UM.Theme.getSize("default_margin").width + Layout.alignment: Qt.AlignHCenter + RowLayout { - id: searchBar - implicitHeight: UM.Theme.getSize("button_icon").height - Layout.fillWidth: true - onTextEdited: searchStringChanged(text) - } + width: parent.width + height: UM.Theme.getSize("button_icon").height + UM.Theme.getSize("default_margin").height + spacing: UM.Theme.getSize("thin_margin").width - // Page selection. - TabBar - { - id: pageSelectionTabBar - Layout.alignment: Qt.AlignRight - height: UM.Theme.getSize("button_icon").height - spacing: 0 - background: Rectangle { color: "transparent" } - currentIndex: manager.tabShown - - onCurrentIndexChanged: + Cura.SearchBar { - manager.tabShown = currentIndex - searchBar.text = ""; - searchBar.visible = currentItem.hasSearch; - content.source = currentItem.sourcePage; + id: searchBar + implicitHeight: UM.Theme.getSize("button_icon").height + Layout.fillWidth: true + onTextEdited: searchStringChanged(text) } - PackageTypeTab + // Page selection. + TabBar { - id: pluginTabText - width: implicitWidth - text: catalog.i18nc("@button", "Plugins") - property string sourcePage: "Plugins.qml" - property bool hasSearch: true - } - PackageTypeTab - { - id: materialsTabText - width: implicitWidth - text: catalog.i18nc("@button", "Materials") - property string sourcePage: "Materials.qml" - property bool hasSearch: true - } - ManagePackagesButton - { - property string sourcePage: "ManagedPackages.qml" - property bool hasSearch: false + id: pageSelectionTabBar + Layout.alignment: Qt.AlignRight + height: UM.Theme.getSize("button_icon").height + spacing: 0 + background: Rectangle { + color: "transparent" + } + currentIndex: manager.tabShown - Cura.NotificationIcon + onCurrentIndexChanged: { - anchors - { - horizontalCenter: parent.right - verticalCenter: parent.top - } - visible: CuraApplication.getPackageManager().packagesWithUpdate.length > 0 + manager.tabShown = currentIndex + searchBar.text = ""; + searchBar.visible = currentItem.hasSearch; + content.source = currentItem.sourcePage; + } - labelText: + PackageTypeTab + { + id: pluginTabText + width: implicitWidth + text: catalog.i18nc("@button", "Plugins") + property string sourcePage: "Plugins.qml" + property bool hasSearch: true + } + PackageTypeTab + { + id: materialsTabText + width: implicitWidth + text: catalog.i18nc("@button", "Materials") + property string sourcePage: "Materials.qml" + property bool hasSearch: true + } + ManagePackagesButton + { + property string sourcePage: "ManagedPackages.qml" + property bool hasSearch: false + + Cura.NotificationIcon { - const itemCount = CuraApplication.getPackageManager().packagesWithUpdate.length - return itemCount > 9 ? "9+" : itemCount + anchors + { + horizontalCenter: parent.right + verticalCenter: parent.top + } + visible: CuraApplication.getPackageManager().packagesWithUpdate.length > 0 + + labelText: + { + const itemCount = CuraApplication.getPackageManager().packagesWithUpdate.length + return itemCount > 9 ? "9+" : itemCount + } } } } } } - } - FontMetrics - { - id: fontMetrics - font: UM.Theme.getFont("default") - } + FontMetrics + { + id: fontMetrics + font: UM.Theme.getFont("default") + } - Cura.TertiaryButton - { - text: catalog.i18nc("@info", "Search in the browser") - iconSource: UM.Theme.getIcon("LinkExternal") - visible: pageSelectionTabBar.currentItem.hasSearch && searchHeader.visible - isIconOnRightSide: true - height: fontMetrics.height - textFont: fontMetrics.font - textColor: UM.Theme.getColor("text") + Cura.TertiaryButton + { + text: catalog.i18nc("@info", "Search in the browser") + iconSource: UM.Theme.getIcon("LinkExternal") + visible: pageSelectionTabBar.currentItem.hasSearch && searchHeader.visible + isIconOnRightSide: true + height: fontMetrics.height + textFont: fontMetrics.font + textColor: UM.Theme.getColor("text") - onClicked: content.item && Qt.openUrlExternally(content.item.searchInBrowserUrl) - } - - // Page contents. - Rectangle - { - Layout.preferredWidth: parent.width - Layout.fillHeight: true - color: UM.Theme.getColor("detail_background") + onClicked: content.item && Qt.openUrlExternally(content.item.searchInBrowserUrl) + } // Page contents. - Loader + Rectangle { - id: content - anchors.fill: parent - anchors.margins: UM.Theme.getSize("default_margin").width - source: "Plugins.qml" + Layout.preferredWidth: parent.width + Layout.fillHeight: true + color: UM.Theme.getColor("detail_background") - Connections + // Page contents. + Loader { - target: content - function onLoaded() + id: content + anchors.fill: parent + anchors.margins: UM.Theme.getSize("default_margin").width + source: "Plugins.qml" + + Connections { - pageTitle.text = content.item.pageTitle - searchStringChanged.connect(handleSearchStringChanged) - } - function handleSearchStringChanged(new_search) - { - content.item.model.searchString = new_search + target: content + + function onLoaded() + { + pageTitle.text = content.item.pageTitle + searchStringChanged.connect(handleSearchStringChanged) + } + + function handleSearchStringChanged(new_search) + { + content.item.model.searchString = new_search + } } } } } } - } - Rectangle - { - height: quitButton.height + 2 * UM.Theme.getSize("default_margin").width - color: UM.Theme.getColor("primary") - visible: manager.showRestartNotification - anchors - { - left: parent.left - right: parent.right - bottom: parent.bottom - } - - RowLayout + Rectangle { + height: quitButton.height + 2 * UM.Theme.getSize("default_margin").width + color: UM.Theme.getColor("primary") + visible: manager.showRestartNotification anchors { left: parent.left right: parent.right - verticalCenter: parent.verticalCenter - margins: UM.Theme.getSize("default_margin").width + bottom: parent.bottom } - spacing: UM.Theme.getSize("default_margin").width - UM.ColorImage - { - id: bannerIcon - source: UM.Theme.getIcon("Plugin") - color: UM.Theme.getColor("primary_button_text") - implicitWidth: UM.Theme.getSize("banner_icon_size").width - implicitHeight: UM.Theme.getSize("banner_icon_size").height - } - Text + RowLayout { - color: UM.Theme.getColor("primary_button_text") - text: catalog.i18nc("@button", "In order to use the package you will need to restart Cura") - font: UM.Theme.getFont("default") - renderType: Text.NativeRendering - Layout.fillWidth: true - } - Cura.SecondaryButton - { - id: quitButton - text: catalog.i18nc("@info:button, %1 is the application name", "Quit %1").arg(CuraApplication.applicationDisplayName) - onClicked: + anchors { - marketplaceDialog.hide(); - CuraApplication.checkAndExitApplication(); + left: parent.left + right: parent.right + verticalCenter: parent.verticalCenter + margins: UM.Theme.getSize("default_margin").width + } + spacing: UM.Theme.getSize("default_margin").width + UM.ColorImage + { + id: bannerIcon + source: UM.Theme.getIcon("Plugin") + + color: UM.Theme.getColor("primary_button_text") + implicitWidth: UM.Theme.getSize("banner_icon_size").width + implicitHeight: UM.Theme.getSize("banner_icon_size").height + } + Text + { + color: UM.Theme.getColor("primary_button_text") + text: catalog.i18nc("@button", "In order to use the package you will need to restart Cura") + font: UM.Theme.getFont("default") + renderType: Text.NativeRendering + Layout.fillWidth: true + } + Cura.SecondaryButton + { + id: quitButton + text: catalog.i18nc("@info:button, %1 is the application name", "Quit %1").arg(CuraApplication.applicationDisplayName) + onClicked: + { + marketplaceDialog.hide(); + CuraApplication.checkAndExitApplication(); + } } } } - } - Rectangle - { - color: UM.Theme.getColor("main_background") - anchors.fill: parent - visible: !Cura.API.account.isLoggedIn && CuraApplication.isEnterprise - - UM.Label + Rectangle { - id: signInLabel - anchors.centerIn: parent - width: Math.round(UM.Theme.getSize("modal_window_minimum").width / 2.5) - text: catalog.i18nc("@description","Please sign in to get verified plugins and materials for UltiMaker Cura Enterprise") - horizontalAlignment: Text.AlignHCenter + color: UM.Theme.getColor("main_background") + anchors.fill: parent + visible: !Cura.API.account.isLoggedIn && CuraApplication.isEnterprise + + UM.Label + { + id: signInLabel + anchors.centerIn: parent + width: Math.round(UM.Theme.getSize("modal_window_minimum").width / 2.5) + text: catalog.i18nc("@description", "Please sign in to get verified plugins and materials for UltiMaker Cura Enterprise") + horizontalAlignment: Text.AlignHCenter + } + + Cura.PrimaryButton + { + id: loginButton + width: UM.Theme.getSize("account_button").width + height: UM.Theme.getSize("account_button").height + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: signInLabel.bottom + anchors.topMargin: UM.Theme.getSize("default_margin").height * 2 + text: catalog.i18nc("@button", "Sign in") + fixedWidthMode: true + onClicked: Cura.API.account.login() + } } - Cura.PrimaryButton + Connections { - id: loginButton - width: UM.Theme.getSize("account_button").width - height: UM.Theme.getSize("account_button").height - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: signInLabel.bottom - anchors.topMargin: UM.Theme.getSize("default_margin").height * 2 - text: catalog.i18nc("@button", "Sign in") - fixedWidthMode: true - onClicked: Cura.API.account.login() + target: Cura.API.account + function onLoginStateChanged() + { + reject(); + } } } } From 2e07629bc127a663ef36589af297e554fc3a8e06 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Fri, 25 Jul 2025 12:19:08 +0200 Subject: [PATCH 121/159] Delete um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg --- ..._cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg | 61 ------------------- 1 file changed, 61 deletions(-) delete mode 100644 resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg deleted file mode 100644 index ed86b8e735..0000000000 --- a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg +++ /dev/null @@ -1,61 +0,0 @@ -[general] -definition = ultimaker_s8 -name = Extra Fast -version = 4 - -[metadata] -material = generic_nylon-cf-slide -quality_type = verydraft -setting_version = 25 -type = quality -variant = CC+ 0.6 -weight = -3 - -[values] -acceleration_roofing = =acceleration_topbottom/2 -bridge_enable_more_layers = True -bridge_skin_density = 70 -bridge_skin_material_flow = 100 -bridge_skin_material_flow_2 = 70 -bridge_skin_speed = 30 -bridge_skin_speed_2 = =speed_print*2/3 -bridge_wall_material_flow = 100 -bridge_wall_min_length = 2 -bridge_wall_speed = 30 -cool_min_layer_time = 6 -cool_min_layer_time_fan_speed_max = 11 -cool_min_layer_time_overhang = 11 -cool_min_temperature = =material_print_temperature-10 -flooring_monotonic = False -infill_material_flow = =material_flow if infill_sparse_density < 95 else 95 -infill_pattern = ='zigzag' if infill_sparse_density > 50 else 'grid' -jerk_roofing = =jerk_print -material_flow = 95 -retraction_hop_enabled = False -retraction_prime_speed = 25 -roofing_line_width = 0.5 -roofing_material_flow = =skin_material_flow -roofing_monotonic = False -skin_material_flow = =0.95*material_flow -skin_outline_count = 0 -speed_print = 55 -speed_roofing = 50 -speed_support_interface = 55 -speed_wall = =speed_print -speed_wall_0_roofing = =speed_roofing -speed_wall_x_roofing = =speed_roofing -support_bottom_distance = =support_z_distance -support_line_width = 0.6 -support_structure = tree -support_tree_tip_diameter = 2.0 -support_tree_top_rate = 10 -support_xy_distance = 1.2 -support_xy_distance_overhang = =1.5*machine_nozzle_size -support_z_distance = =min(2*layer_height, 0.4) -top_bottom_thickness = =wall_thickness -wall_0_inset = =0.05 -wall_line_width_0 = 0.5 -wall_overhang_speed_factors = [100,90,80,70,60,50] -wall_x_material_flow = =material_flow -xy_offset = 0.075 - From c9857930395b3c4a94c8b180201744a7d8366216 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Fri, 25 Jul 2025 12:19:10 +0200 Subject: [PATCH 122/159] Delete um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg --- .../um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg | 65 ------------------- 1 file changed, 65 deletions(-) delete mode 100644 resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg deleted file mode 100644 index 9f5a1add1f..0000000000 --- a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg +++ /dev/null @@ -1,65 +0,0 @@ -[general] -definition = ultimaker_s8 -name = Extra Fast -version = 4 - -[metadata] -material = generic_petcf -quality_type = verydraft -setting_version = 25 -type = quality -variant = CC+ 0.6 -weight = -3 - -[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 -material_print_temperature = =default_material_print_temperature + 10 -retraction_hop_enabled = False -retraction_prime_speed = 15 -roofing_line_width = 0.5 -roofing_material_flow = =skin_material_flow -roofing_monotonic = False -skin_material_flow = =0.95*material_flow -skin_outline_count = 0 -skirt_height = 5 -speed_print = 55 -speed_roofing = 50 -speed_support_interface = 55 -speed_wall = =speed_print -speed_wall_0_roofing = =speed_roofing -speed_wall_x_roofing = =speed_roofing -support_bottom_distance = =support_z_distance -support_interface_enable = False -support_line_width = 0.6 -support_structure = tree -support_tree_tip_diameter = 2.0 -support_tree_top_rate = 10 -support_xy_distance = 1.2 -support_xy_distance_overhang = =1.5*machine_nozzle_size -support_z_distance = =min(2*layer_height, 0.4) -switch_extruder_retraction_amount = 16 -top_bottom_thickness = =wall_thickness -wall_0_inset = =0.05 -wall_line_width_0 = =line_width * (1 + magic_spiralize * 0.25) -wall_overhang_speed_factors = [100,90,80,70,60,50] -wall_x_material_flow = =material_flow -xy_offset = 0.075 - From 376d18c7ee7d7abdf151b75ee79213d51c0d679f Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 25 Jul 2025 13:21:06 +0200 Subject: [PATCH 123/159] Change Preferences dialog creation and destruction CURA-11810 --- resources/qml/Cura.qml | 55 +++---- .../qml/Preferences/PreferencesDialog.qml | 136 ++++++++++++++++++ resources/qml/qmldir | 4 + 3 files changed, 161 insertions(+), 34 deletions(-) create mode 100644 resources/qml/Preferences/PreferencesDialog.qml diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 77efc45fe8..bc5cc6a044 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -456,45 +456,32 @@ UM.MainWindow } } - UM.PreferencesDialog + Component { - id: preferences - - Component.onCompleted: + id: preferencesDialogComponent + Cura.PreferencesDialog { - //; Remove & re-add the general page as we want to use our own instead of uranium standard. - removePage(0); - insertPage(0, catalog.i18nc("@title:tab","General"), Qt.resolvedUrl("Preferences/GeneralPage.qml")); - - removePage(1); - insertPage(1, catalog.i18nc("@title:tab","Settings"), Qt.resolvedUrl("Preferences/SettingVisibilityPage.qml")); - - insertPage(2, catalog.i18nc("@title:tab", "Printers"), Qt.resolvedUrl("Preferences/MachinesPage.qml")); - - insertPage(3, catalog.i18nc("@title:tab", "Materials"), Qt.resolvedUrl("Preferences/Materials/MaterialsPage.qml")); - - insertPage(4, catalog.i18nc("@title:tab", "Profiles"), Qt.resolvedUrl("Preferences/ProfilesPage.qml")); - currentPage = 0; + selfDestroy: true } + } - onVisibleChanged: - { - // When the dialog closes, switch to the General page. - // This prevents us from having a heavy page like Setting Visibility active in the background. - setPage(0); - } + function showPreferencesDialog() + { + var dialog = preferencesDialogComponent.createObject(base) + dialog.show() + return dialog } Connections { target: Cura.Actions.preferences - function onTriggered() { preferences.visible = true } + function onTriggered() { showPreferencesDialog() } } Connections { target: CuraApplication - function onShowPreferencesWindow() { preferences.visible = true } + function onShowPreferencesWindow() { showPreferencesDialog() } } Connections @@ -511,8 +498,8 @@ UM.MainWindow target: Cura.Actions.configureMachines function onTriggered() { - preferences.visible = true; - preferences.setPage(2); + var dialog = showPreferencesDialog() + dialog.currentPage = 2; } } @@ -521,8 +508,8 @@ UM.MainWindow target: Cura.Actions.manageProfiles function onTriggered() { - preferences.visible = true; - preferences.setPage(4); + var dialog = showPreferencesDialog() + dialog.currentPage = 4; } } @@ -531,8 +518,8 @@ UM.MainWindow target: Cura.Actions.manageMaterials function onTriggered() { - preferences.visible = true; - preferences.setPage(3) + var dialog = showPreferencesDialog() + dialog.currentPage = 3; } } @@ -541,11 +528,11 @@ UM.MainWindow target: Cura.Actions.configureSettingVisibility function onTriggered(source) { - preferences.visible = true; - preferences.setPage(1); + var dialog = showPreferencesDialog() + dialog.currentPage = 1; if(source && source.key) { - preferences.getCurrentItem().scrollToSection(source.key); + dialog.currentItem.scrollToSection(source.key); } } } diff --git a/resources/qml/Preferences/PreferencesDialog.qml b/resources/qml/Preferences/PreferencesDialog.qml new file mode 100644 index 0000000000..0d46cd6b42 --- /dev/null +++ b/resources/qml/Preferences/PreferencesDialog.qml @@ -0,0 +1,136 @@ +// Copyright (c) 2022 Ultimaker B.V. +// Uranium is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.1 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.1 + +import ".." + +import UM 1.6 as UM + +UM.Dialog +{ + id: base + + title: catalog.i18nc("@title:window", "Preferences") + minimumWidth: UM.Theme.getSize("modal_window_minimum").width + minimumHeight: UM.Theme.getSize("modal_window_minimum").height + width: minimumWidth + height: minimumHeight + + property alias currentPage: pagesList.currentIndex + property alias currentItem: pagesList.currentItem + + Rectangle + { + anchors.fill: parent + color: UM.Theme.getColor("background_2") + } + + Item + { + id: test + anchors.fill: parent + anchors.margins: UM.Theme.getSize("default_margin").width + + ListView + { + id: pagesList + width: UM.Theme.getSize("preferences_page_list_item").width + anchors.top: parent.top + anchors.bottom: parent.bottom + + ScrollBar.vertical: UM.ScrollBar {} + clip: true + model: [ + { + name: catalog.i18nc("@title:tab","General"), + item: Qt.resolvedUrl("GeneralPage.qml") + }, + { + name: catalog.i18nc("@title:tab","Settings"), + item: Qt.resolvedUrl("SettingVisibilityPage.qml") + }, + { + name: catalog.i18nc("@title:tab","Printers"), + item: Qt.resolvedUrl("MachinesPage.qml") + }, + { + name: catalog.i18nc("@title:tab","Materials"), + item: Qt.resolvedUrl("Materials/MaterialsPage.qml") + }, + { + name: catalog.i18nc("@title:tab","Profiles"), + item: Qt.resolvedUrl("ProfilesPage.qml") + } + ] + + delegate: Rectangle + { + width: parent ? parent.width : 0 + height: pageLabel.height + + color: ListView.isCurrentItem ? UM.Theme.getColor("background_3") : UM.Theme.getColor("main_background") + + UM.Label + { + id: pageLabel + anchors.centerIn: parent + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + width: parent.width + height: UM.Theme.getSize("preferences_page_list_item").height + color: UM.Theme.getColor("text_default") + text: modelData.name + } + MouseArea + { + anchors.fill: parent + onClicked: pagesList.currentIndex = index + } + } + + onCurrentIndexChanged: stackView.replace(model[currentIndex].item) + } + + StackView + { + id: stackView + anchors + { + left: pagesList.right + leftMargin: UM.Theme.getSize("narrow_margin").width + top: parent.top + bottom: parent.bottom + right: parent.right + } + + initialItem: Item { property bool resetEnabled: false } + + replaceEnter: Transition + { + NumberAnimation + { + properties: "opacity" + from: 0 + to: 1 + duration: 100 + } + } + replaceExit: Transition + { + NumberAnimation + { + properties: "opacity" + from: 1 + to: 0 + duration: 100 + } + } + } + + UM.I18nCatalog { id: catalog; name: "uranium"; } + } +} diff --git a/resources/qml/qmldir b/resources/qml/qmldir index 8fce82c858..98140608c6 100644 --- a/resources/qml/qmldir +++ b/resources/qml/qmldir @@ -52,3 +52,7 @@ NumericTextFieldWithUnit 1.0 NumericTextFieldWithUnit.qml PrintHeadMinMaxTextField 1.0 PrintHeadMinMaxTextField.qml SimpleCheckBox 1.0 SimpleCheckBox.qml RenameDialog 1.0 RenameDialog.qml + +# Cura/Preferences + +PreferencesDialog 1.0 PreferencesDialog.qml From d00a9b8715fb9eaa8f0c4b6b605de2f684bb69d4 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 25 Jul 2025 14:15:55 +0200 Subject: [PATCH 124/159] Fix double margin in Preferences dialog CURA-11810 --- .../qml/Preferences/PreferencesDialog.qml | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/resources/qml/Preferences/PreferencesDialog.qml b/resources/qml/Preferences/PreferencesDialog.qml index 0d46cd6b42..f175370205 100644 --- a/resources/qml/Preferences/PreferencesDialog.qml +++ b/resources/qml/Preferences/PreferencesDialog.qml @@ -19,21 +19,15 @@ UM.Dialog minimumHeight: UM.Theme.getSize("modal_window_minimum").height width: minimumWidth height: minimumHeight + backgroundColor: UM.Theme.getColor("background_2") property alias currentPage: pagesList.currentIndex property alias currentItem: pagesList.currentItem - Rectangle - { - anchors.fill: parent - color: UM.Theme.getColor("background_2") - } - Item { id: test anchors.fill: parent - anchors.margins: UM.Theme.getSize("default_margin").width ListView { @@ -46,23 +40,23 @@ UM.Dialog clip: true model: [ { - name: catalog.i18nc("@title:tab","General"), + name: catalog.i18nc("@title:tab", "General"), item: Qt.resolvedUrl("GeneralPage.qml") }, { - name: catalog.i18nc("@title:tab","Settings"), + name: catalog.i18nc("@title:tab", "Settings"), item: Qt.resolvedUrl("SettingVisibilityPage.qml") }, { - name: catalog.i18nc("@title:tab","Printers"), + name: catalog.i18nc("@title:tab", "Printers"), item: Qt.resolvedUrl("MachinesPage.qml") }, { - name: catalog.i18nc("@title:tab","Materials"), + name: catalog.i18nc("@title:tab", "Materials"), item: Qt.resolvedUrl("Materials/MaterialsPage.qml") }, { - name: catalog.i18nc("@title:tab","Profiles"), + name: catalog.i18nc("@title:tab", "Profiles"), item: Qt.resolvedUrl("ProfilesPage.qml") } ] From 4dca70bf29c8f6def45f4bf8226c017b45a2c1fe Mon Sep 17 00:00:00 2001 From: MariMakes <40423138+MariMakes@users.noreply.github.com> Date: Fri, 25 Jul 2025 17:32:09 +0200 Subject: [PATCH 125/159] Updated changelog for the patch Updated changelog for the patch. Contributes to CURA-12640 --- resources/texts/change_log.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/resources/texts/change_log.txt b/resources/texts/change_log.txt index c432a9e309..4229c0869e 100644 --- a/resources/texts/change_log.txt +++ b/resources/texts/change_log.txt @@ -1,3 +1,9 @@ +[5.10.2] + +- Introduced the CC+ 0.6 core to the UlitMaker S6 and S8. This core delivers better results for demanding applications and will be replacing the CC+ 0.4 core. +- Added new profiles for PC and CPE+ on UltiMaker S6 and UltiMaker S8 +- Updated the default support type for the PETG material for UltiMaker S6 and UltiMaker S8 + [5.10.1] * New features and improvements: From f8810c463ac6d02cf0a79e11f7592fd9d600a7a9 Mon Sep 17 00:00:00 2001 From: Wouter Symons Date: Sun, 27 Jul 2025 15:30:32 +0200 Subject: [PATCH 126/159] conanfile.py: correct quotes in f-string --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 89b2da35d6..dbe524b2f1 100644 --- a/conanfile.py +++ b/conanfile.py @@ -338,7 +338,7 @@ class CuraConan(ConanFile): if extra_build_identifiers: separator = "+" if not cura_version.build else "." - cura_version = Version(f"{cura_version}{separator}{".".join(extra_build_identifiers)}") + cura_version = Version(f"{cura_version}{separator}{'.'.join(extra_build_identifiers)}") self.output.info(f"Write CuraVersion.py to {self.recipe_folder}") From a866542a324e0079381cd01f463aa277e28f0061 Mon Sep 17 00:00:00 2001 From: Mariska <40423138+MariMakes@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:15:13 +0200 Subject: [PATCH 127/159] Apply suggestions from code review Applied suggested improvements (and typo fixes) Co-authored-by: Erwan MATHIEU --- resources/texts/change_log.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/texts/change_log.txt b/resources/texts/change_log.txt index 4229c0869e..b4dd6bef16 100644 --- a/resources/texts/change_log.txt +++ b/resources/texts/change_log.txt @@ -1,8 +1,8 @@ [5.10.2] -- Introduced the CC+ 0.6 core to the UlitMaker S6 and S8. This core delivers better results for demanding applications and will be replacing the CC+ 0.4 core. -- Added new profiles for PC and CPE+ on UltiMaker S6 and UltiMaker S8 -- Updated the default support type for the PETG material for UltiMaker S6 and UltiMaker S8 +- Introduced the CC+ 0.6 core to the UltiMaker S6 and S8. This core delivers better results for demanding applications and will be replacing the CC+ 0.4 core. +- Added new profiles for PC and CPE+ on UltiMaker S6 and S8 +- Updated the default support type for the PETG material for UltiMaker S6 and S8 [5.10.1] From 17600c2660ec50bb1968df44c37bdc1bce3758cc Mon Sep 17 00:00:00 2001 From: Mariska <40423138+MariMakes@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:17:58 +0200 Subject: [PATCH 128/159] Added category added a * --- resources/texts/change_log.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/texts/change_log.txt b/resources/texts/change_log.txt index b4dd6bef16..af983e621c 100644 --- a/resources/texts/change_log.txt +++ b/resources/texts/change_log.txt @@ -1,5 +1,6 @@ [5.10.2] +* UltiMaker S6 and S8 improvements: - Introduced the CC+ 0.6 core to the UltiMaker S6 and S8. This core delivers better results for demanding applications and will be replacing the CC+ 0.4 core. - Added new profiles for PC and CPE+ on UltiMaker S6 and S8 - Updated the default support type for the PETG material for UltiMaker S6 and S8 From 5426f80c57e687f3e1da75f0917bde4b70f7b0a6 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 29 Jul 2025 07:54:30 +0200 Subject: [PATCH 129/159] Update action version --- .github/workflows/printer-linter-pr-diagnose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/printer-linter-pr-diagnose.yml b/.github/workflows/printer-linter-pr-diagnose.yml index 64892e0db1..666383c8f9 100644 --- a/.github/workflows/printer-linter-pr-diagnose.yml +++ b/.github/workflows/printer-linter-pr-diagnose.yml @@ -47,7 +47,7 @@ jobs: path: printer-linter-result/ - name: Run clang-tidy-pr-comments action - uses: platisd/clang-tidy-pr-comments@bc0bb7da034a8317d54e7fe1e819159002f4cc40 + uses: platisd/clang-tidy-pr-comments@v1.8.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} clang_tidy_fixes: result.yml From 92ff625a52e8dd46b720cecbc623650dca6cef09 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 29 Jul 2025 19:18:39 +0200 Subject: [PATCH 130/159] Add UV-unwrap lib to 'hidden imports'. This is probably what it needs for the builds to start working on Windows. part of CURA-12528 --- conandata.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/conandata.yml b/conandata.yml index 3181d601e5..537c8bc72c 100644 --- a/conandata.yml +++ b/conandata.yml @@ -99,6 +99,7 @@ pyinstaller: - "pyArcus" - "pyDulcificum" - "pynest2d" + - "pyUvula" - "PyQt6" - "PyQt6.QtNetwork" - "PyQt6.sip" From 5d2aca4e31fbf57c19fbff58ae0e884f670fe334 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 30 Jul 2025 12:10:40 +0200 Subject: [PATCH 131/159] Avoid crash when UV coordinates not loaded/generated CURA-12664 --- plugins/PaintTool/PaintTool.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index 524011af9d..fa6436f10d 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -186,7 +186,11 @@ class PaintTool(Tool): pt = self._picking_pass.getPickedPosition(x, y).getData() va, vb, vc = self._mesh_transformed_cache.getFaceNodes(face_id) - ta, tb, tc = node.getMeshData().getFaceUvCoords(face_id) + + face_uv_coordinates = node.getMeshData().getFaceUvCoords(face_id) + if face_uv_coordinates is None: + return face_id, None + ta, tb, tc = face_uv_coordinates # 'Weight' of each vertex that would produce point pt, so we can generate the texture coordinates from the uv ones of the vertices. # See (also) https://mathworld.wolfram.com/BarycentricCoordinates.html From 8af8283d2c00c2c1b968f934c4a18df6c5b310ac Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 30 Jul 2025 14:02:56 +0200 Subject: [PATCH 132/159] Use newly exposed 'reloadNodes' so we can use Uraniums version of that. This caused issues where the code deleted in this ticket was almost the same as in Uranium, except it was slightly buggy (but it also did slightly more, hence the new 'on_done' parametrer). part of CURA-12630 --- cura/CuraApplication.py | 58 +++-------------------------------------- 1 file changed, 3 insertions(+), 55 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 8af98c2d0e..491d68630e 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -9,7 +9,6 @@ import time import platform from pathlib import Path from typing import cast, TYPE_CHECKING, Optional, Callable, List, Any, Dict -import requests import numpy from PyQt6.QtCore import QObject, QTimer, QUrl, QUrlQuery, pyqtSignal, pyqtProperty, QEvent, pyqtEnum, QCoreApplication, \ @@ -1645,14 +1644,10 @@ class CuraApplication(QtApplication): Logger.log("w", "Unable to reload data because we don't have a filename.") for file_name, nodes in objects_in_filename.items(): - file_path = os.path.normpath(os.path.dirname(file_name)) - job = ReadMeshJob(file_name, - add_to_recent_files=file_path != tempfile.gettempdir()) # Don't add temp files to the recent files list - job._nodes = nodes # type: ignore - job.finished.connect(self._reloadMeshFinished) + on_done = None if has_merged_nodes: - job.finished.connect(self.updateOriginOfMergedMeshes) - job.start() + on_done = self.updateOriginOfMergedMeshes + self.getController().getScene().reloadNodes(nodes, file_name, on_done) @pyqtSlot("QStringList") def setExpandedCategories(self, categories: List[str]) -> None: @@ -1835,53 +1830,6 @@ class CuraApplication(QtApplication): fileLoaded = pyqtSignal(str) fileCompleted = pyqtSignal(str) - def _reloadMeshFinished(self, job) -> None: - """ - Function called when ReadMeshJob finishes reloading a file in the background, then update node objects in the - scene from its source file. The function gets all the nodes that exist in the file through the job result, and - then finds the scene nodes that need to be refreshed by their name. Each job refreshes all nodes of a file. - Nodes that are not present in the updated file are kept in the scene. - - :param job: The :py:class:`Uranium.UM.ReadMeshJob.ReadMeshJob` running in the background that reads all the - meshes in a file - """ - - job_result = job.getResult() # nodes that exist inside the file read by this job - if len(job_result) == 0: - Logger.log("e", "Reloading the mesh failed.") - return - renamed_nodes = {} # type: Dict[str, int] - # Find the node to be refreshed based on its id - for job_result_node in job_result: - mesh_data = job_result_node.getMeshData() - if not mesh_data: - Logger.log("w", "Could not find a mesh in reloaded node.") - continue - - # Solves issues with object naming - result_node_name = job_result_node.getName() - if not result_node_name: - result_node_name = os.path.basename(mesh_data.getFileName()) - if result_node_name in renamed_nodes: # objects may get renamed by ObjectsModel._renameNodes() when loaded - renamed_nodes[result_node_name] += 1 - result_node_name = "{0}({1})".format(result_node_name, renamed_nodes[result_node_name]) - else: - renamed_nodes[job_result_node.getName()] = 0 - - # Find the matching scene node to replace - scene_node = None - for replaced_node in job._nodes: - if replaced_node.getName() == result_node_name: - scene_node = replaced_node - break - - if scene_node: - scene_node.setMeshData(mesh_data) - else: - # Current node is a new one in the file, or it's name has changed - # TODO: Load this mesh into the scene. Also alter the "_reloadJobFinished" action in UM.Scene - Logger.log("w", "Could not find matching node for object '{0}' in the scene.".format(result_node_name)) - def _openFile(self, filename): self.readLocalFile(QUrl.fromLocalFile(filename)) From 37c364e8a5692bc3b7fd950a586a8918589132da Mon Sep 17 00:00:00 2001 From: THeijmans Date: Wed, 30 Jul 2025 14:11:57 +0200 Subject: [PATCH 133/159] PP-650-High-speed-profile-improvements-S6S8 --- resources/definitions/ultimaker_s8.def.json | 61 +++++++++++-------- ...um_s8_aa_plus_0.4_abs_0.2mm_quick.inst.cfg | 22 +++++++ ...m_s8_aa_plus_0.4_petg_0.2mm_quick.inst.cfg | 22 +++++++ ...um_s8_aa_plus_0.4_pla_0.2mm_quick.inst.cfg | 22 +++++++ ...aa_plus_0.4_tough-pla_0.2mm_quick.inst.cfg | 22 +++++++ .../um_s8_aa_plus_0.4_abs_0.2mm.inst.cfg | 5 +- .../um_s8_aa_plus_0.4_petg_0.2mm.inst.cfg | 4 +- .../um_s8_aa_plus_0.4_pla_0.15mm.inst.cfg | 4 ++ .../um_s8_aa_plus_0.4_pla_0.1mm.inst.cfg | 4 ++ .../um_s8_aa_plus_0.4_pla_0.2mm.inst.cfg | 4 ++ ...m_s8_aa_plus_0.4_tough-pla_0.15mm.inst.cfg | 4 ++ ...um_s8_aa_plus_0.4_tough-pla_0.1mm.inst.cfg | 4 ++ ...um_s8_aa_plus_0.4_tough-pla_0.2mm.inst.cfg | 4 ++ 13 files changed, 156 insertions(+), 26 deletions(-) create mode 100644 resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm_quick.inst.cfg create mode 100644 resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm_quick.inst.cfg create mode 100644 resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm_quick.inst.cfg create mode 100644 resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm_quick.inst.cfg diff --git a/resources/definitions/ultimaker_s8.def.json b/resources/definitions/ultimaker_s8.def.json index b7de27722d..cbd6e255f8 100644 --- a/resources/definitions/ultimaker_s8.def.json +++ b/resources/definitions/ultimaker_s8.def.json @@ -98,7 +98,7 @@ { "maximum_value": "machine_max_acceleration_x", "maximum_value_warning": "machine_max_acceleration_x*0.8", - "value": "acceleration_wall_0" + "value": "acceleration_topbottom / 2" }, "acceleration_skirt_brim": { @@ -199,15 +199,19 @@ }, "adhesion_type": { "value": "'brim' if support_enable and support_structure=='tree' else 'skirt'" }, "bottom_thickness": { "value": "3*layer_height if top_layers==4 and not support_enable else top_bottom_thickness" }, - "bridge_skin_material_flow": { "value": 200 }, + "bridge_enable_more_layers": { "value": true }, + "bridge_skin_density": { "value": 70 }, + "bridge_skin_material_flow": { "value": 150 }, + "bridge_skin_material_flow_2": { "value": 70 }, "bridge_skin_speed": { "unit": "mm/s", - "value": "bridge_wall_speed" + "value": 35 }, + "bridge_skin_speed_2": { "value": "speed_print*2/3" }, "bridge_sparse_infill_max_density": { "value": 50 }, - "bridge_wall_material_flow": { "value": "bridge_skin_material_flow" }, - "bridge_wall_min_length": { "value": 10 }, + "bridge_wall_material_flow": { "value": 200 }, + "bridge_wall_min_length": { "value": 2 }, "bridge_wall_speed": { "unit": "mm/s", @@ -221,13 +225,13 @@ ] }, "cool_during_extruder_switch": { "value": "'all_fans'" }, - "cool_min_layer_time": { "value": 5 }, - "cool_min_layer_time_overhang": { "value": 9 }, - "cool_min_layer_time_overhang_min_segment_length": { "value": 2 }, + "cool_min_layer_time": { "value": 6 }, + "cool_min_layer_time_overhang": { "value": 11 }, + "cool_min_layer_time_overhang_min_segment_length": { "value": 1.5 }, "cool_min_speed": { "value": 6 }, "cool_min_temperature": { - "minimum_value_warning": "material_print_temperature-15", + "minimum_value_warning": "material_print_temperature-20", "value": "material_print_temperature-15" }, "default_material_print_temperature": { "maximum_value_warning": 320 }, @@ -235,9 +239,9 @@ "flooring_layer_count": { "value": 1 }, "gradual_flow_enabled": { "value": false }, "hole_xy_offset": { "value": 0.075 }, - "infill_material_flow": { "value": "material_flow" }, + "infill_material_flow": { "value": "material_flow if infill_sparse_density < 95 else 95" }, "infill_overlap": { "value": 10 }, - "infill_pattern": { "value": "'zigzag' if infill_sparse_density > 80 else 'grid'" }, + "infill_pattern": { "value": "'zigzag' if infill_sparse_density > 50 else 'grid'" }, "infill_sparse_density": { "value": 15 }, "infill_wall_line_count": { "value": "1 if infill_sparse_density > 80 else 0" }, "initial_bottom_layers": { "value": 2 }, @@ -281,7 +285,7 @@ { "maximum_value_warning": "machine_max_jerk_xy / 2", "unit": "m/s\u00b3", - "value": "jerk_wall_0" + "value": "jerk_print" }, "jerk_skirt_brim": { @@ -438,10 +442,11 @@ "retraction_hop": { "value": 1 }, "retraction_hop_after_extruder_switch_height": { "value": 2 }, "retraction_hop_enabled": { "value": true }, - "retraction_min_travel": { "value": "5 if support_enable and support_structure=='tree' else line_width * 2.5" }, + "retraction_min_travel": { "value": "2.5 if support_enable and support_structure=='tree' else line_width * 2.5" }, "retraction_prime_speed": { "value": 15 }, "skin_edge_support_thickness": { "value": 0 }, - "skin_material_flow": { "value": 95 }, + "skin_material_flow": { "value": 93 }, + "skin_outline_count": { "value": 0 }, "skin_overlap": { "value": 0 }, "skin_preshrink": { "value": 0 }, "skirt_brim_minimal_length": { "value": 1000 }, @@ -571,38 +576,46 @@ "value": "speed_wall" }, "support_angle": { "value": 60 }, - "support_bottom_distance": { "maximum_value_warning": "3*layer_height" }, + "support_bottom_distance": + { + "maximum_value_warning": "3*layer_height", + "value": "support_z_distance" + }, "support_bottom_offset": { "value": 0 }, "support_brim_width": { "value": 10 }, "support_interface_enable": { "value": true }, "support_interface_offset": { "value": "support_offset" }, "support_line_width": { "value": "1.25*line_width" }, - "support_offset": { "value": "1.2 if support_structure == 'tree' else 0.8" }, + "support_offset": { "value": 0.8 }, "support_pattern": { "value": "'gyroid' if support_structure == 'tree' else 'lines'" }, "support_roof_height": { "minimum_value_warning": 0 }, "support_structure": { "value": "'normal'" }, "support_top_distance": { "maximum_value_warning": "3*layer_height" }, "support_tree_angle": { "value": 50 }, "support_tree_angle_slow": { "value": 35 }, - "support_tree_bp_diameter": { "value": 15 }, - "support_tree_branch_diameter": { "value": 8 }, - "support_tree_tip_diameter": { "value": 1.0 }, - "support_tree_top_rate": { "value": 20 }, - "support_xy_distance_overhang": { "value": "machine_nozzle_size" }, - "support_z_distance": { "value": "0.4*material_shrinkage_percentage_z/100.0" }, - "top_bottom_thickness": { "value": "round(4*layer_height, 2)" }, + "support_tree_bp_diameter": { "value": 20 }, + "support_tree_branch_diameter": { "value": 5 }, + "support_tree_branch_diameter_angle": { "value": 5 }, + "support_tree_max_diameter": { "value": 15 }, + "support_tree_tip_diameter": { "value": 2.0 }, + "support_tree_top_rate": { "value": 10 }, + "support_xy_distance": { "value": 1.2 }, + "support_xy_distance_overhang": { "value": "1.5*machine_nozzle_size" }, + "support_z_distance": { "value": "2*layer_height" }, + "top_bottom_thickness": { "value": "wall_thickness" }, "travel_avoid_other_parts": { "value": true }, "travel_avoid_supports": { "value": true }, "wall_0_acceleration": { "value": 1000 }, "wall_0_deceleration": { "value": 1000 }, "wall_0_end_speed_ratio": { "value": 100 }, + "wall_0_inset": { "value": 0.05 }, "wall_0_speed_split_distance": { "value": 0.2 }, "wall_0_start_speed_ratio": { "value": 100 }, "wall_0_wipe_dist": { "value": 0 }, "wall_material_flow": { "value": 95 }, "wall_overhang_angle": { "value": 45 }, "wall_x_material_flow": { "value": 100 }, - "xy_offset": { "value": 0.05 }, + "xy_offset": { "value": 0.075 }, "z_seam_corner": { "value": "'z_seam_corner_weighted'" }, "z_seam_position": { "value": "'backright'" }, "z_seam_type": { "value": "'sharpest_corner'" } diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm_quick.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm_quick.inst.cfg new file mode 100644 index 0000000000..9889424797 --- /dev/null +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm_quick.inst.cfg @@ -0,0 +1,22 @@ +[general] +definition = ultimaker_s8 +name = Quick +version = 4 + +[metadata] +intent_category = quick +material = generic_abs +quality_type = draft +setting_version = 25 +type = intent +variant = AA+ 0.4 + +[values] +cool_min_layer_time = 5 +cool_min_layer_time_overhang = 9 +cool_min_speed = 6 +cool_min_temperature = =material_print_temperature - 15 +speed_wall_x = =speed_print +speed_wall_x_roofing = =speed_wall +wall_line_width_x = =wall_line_width + diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm_quick.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm_quick.inst.cfg new file mode 100644 index 0000000000..ae38c02395 --- /dev/null +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm_quick.inst.cfg @@ -0,0 +1,22 @@ +[general] +definition = ultimaker_s8 +name = Quick +version = 4 + +[metadata] +intent_category = quick +material = generic_petg +quality_type = draft +setting_version = 25 +type = intent +variant = AA+ 0.4 + +[values] +cool_min_layer_time = 5 +cool_min_layer_time_overhang = 9 +cool_min_speed = 6 +cool_min_temperature = =material_print_temperature - 15 +speed_wall_x = =speed_print +speed_wall_x_roofing = =speed_wall +wall_line_width_x = =wall_line_width + diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm_quick.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm_quick.inst.cfg new file mode 100644 index 0000000000..4509317076 --- /dev/null +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm_quick.inst.cfg @@ -0,0 +1,22 @@ +[general] +definition = ultimaker_s8 +name = Quick +version = 4 + +[metadata] +intent_category = quick +material = generic_pla +quality_type = draft +setting_version = 25 +type = intent +variant = AA+ 0.4 + +[values] +cool_min_layer_time = 5 +cool_min_layer_time_overhang = 9 +cool_min_speed = 6 +cool_min_temperature = =material_print_temperature - 15 +speed_wall_x = =speed_print +speed_wall_x_roofing = =speed_wall +wall_line_width_x = =wall_line_width + diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm_quick.inst.cfg b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm_quick.inst.cfg new file mode 100644 index 0000000000..4f75b631df --- /dev/null +++ b/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm_quick.inst.cfg @@ -0,0 +1,22 @@ +[general] +definition = ultimaker_s8 +name = Quick +version = 4 + +[metadata] +intent_category = quick +material = generic_tough_pla +quality_type = draft +setting_version = 25 +type = intent +variant = AA+ 0.4 + +[values] +cool_min_layer_time = 5 +cool_min_layer_time_overhang = 9 +cool_min_speed = 6 +cool_min_temperature = =material_print_temperature - 15 +speed_wall_x = =speed_print +speed_wall_x_roofing = =speed_wall +wall_line_width_x = =wall_line_width + diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm.inst.cfg index 37767673aa..cc5e850220 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm.inst.cfg @@ -14,7 +14,10 @@ weight = -2 [values] cool_min_layer_time = 4 cool_min_layer_time_fan_speed_max = 9 -cool_min_temperature = =material_print_temperature - 10 +cool_min_temperature = =material_print_temperature - 20 retraction_prime_speed = 15 +speed_wall_x = =speed_wall +speed_wall_x_roofing = =speed_wall * 0.8 support_structure = tree +wall_line_width_x = =wall_line_width * 1.25 diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm.inst.cfg index a22e4fbeec..9abcd5ddd2 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_petg_0.2mm.inst.cfg @@ -15,5 +15,7 @@ weight = -2 cool_min_layer_time = 4 material_print_temperature = =default_material_print_temperature + 5 retraction_prime_speed = 15 -support_structure = tree +speed_wall_x = =speed_wall +speed_wall_x_roofing = =speed_wall * 0.8 +wall_line_width_x = =wall_line_width * 1.25 diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.15mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.15mm.inst.cfg index 6c445180f8..9feab61e0e 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.15mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.15mm.inst.cfg @@ -12,8 +12,12 @@ variant = AA+ 0.4 weight = -1 [values] +cool_min_temperature = =material_print_temperature - 20 material_final_print_temperature = =material_print_temperature - 15 material_initial_print_temperature = =material_print_temperature - 15 retraction_prime_speed = =retraction_speed +speed_wall_x = =speed_wall +speed_wall_x_roofing = =speed_wall * 0.8 support_structure = tree +wall_line_width_x = =wall_line_width * 1.25 diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.1mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.1mm.inst.cfg index d3d99eec9e..8431bb9c43 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.1mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.1mm.inst.cfg @@ -12,9 +12,13 @@ variant = AA+ 0.4 weight = 0 [values] +cool_min_temperature = =material_print_temperature - 20 material_final_print_temperature = =material_print_temperature - 15 material_initial_print_temperature = =material_print_temperature - 15 retraction_prime_speed = =retraction_speed +speed_wall_x = =speed_wall +speed_wall_x_roofing = =speed_wall * 0.8 support_structure = tree top_bottom_thickness = =round(6*layer_height,3) +wall_line_width_x = =wall_line_width * 1.25 diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm.inst.cfg index 2e015f8a88..7a5d19dc2c 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_pla_0.2mm.inst.cfg @@ -12,8 +12,12 @@ variant = AA+ 0.4 weight = -2 [values] +cool_min_temperature = =material_print_temperature - 20 material_final_print_temperature = =material_print_temperature - 15 material_initial_print_temperature = =material_print_temperature - 15 retraction_prime_speed = =retraction_speed +speed_wall_x = =speed_wall +speed_wall_x_roofing = =speed_wall * 0.8 support_structure = tree +wall_line_width_x = =wall_line_width * 1.25 diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.15mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.15mm.inst.cfg index e3477c1e7d..f17d3fde40 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.15mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.15mm.inst.cfg @@ -12,7 +12,11 @@ variant = AA+ 0.4 weight = -1 [values] +cool_min_temperature = =material_print_temperature - 20 retraction_prime_speed = =retraction_speed retraction_speed = 25 +speed_wall_x = =speed_wall +speed_wall_x_roofing = =speed_wall * 0.8 support_structure = tree +wall_line_width_x = =wall_line_width * 1.25 diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.1mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.1mm.inst.cfg index 6ccc0da6dd..672eae3e4a 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.1mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.1mm.inst.cfg @@ -12,7 +12,11 @@ variant = AA+ 0.4 weight = 0 [values] +cool_min_temperature = =material_print_temperature - 20 retraction_prime_speed = =retraction_speed +speed_wall_x = =speed_wall +speed_wall_x_roofing = =speed_wall * 0.8 support_structure = tree top_bottom_thickness = =round(6*layer_height,3) +wall_line_width_x = =wall_line_width * 1.25 diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm.inst.cfg index bb629e0758..716765aac5 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_tough-pla_0.2mm.inst.cfg @@ -12,6 +12,10 @@ variant = AA+ 0.4 weight = -2 [values] +cool_min_temperature = =material_print_temperature - 20 retraction_prime_speed = =retraction_speed +speed_wall_x = =speed_wall +speed_wall_x_roofing = =speed_wall * 0.8 support_structure = tree +wall_line_width_x = =wall_line_width * 1.25 From 5040e7f230b031ece60ccbee3cf6224a0567cf74 Mon Sep 17 00:00:00 2001 From: Timur Seitosmanov Date: Wed, 3 Jul 2024 12:10:43 +0200 Subject: [PATCH 134/159] Trigger machine error checking during startup. Otherwise slicing will keep failing until selected printer is changed. --- cura/Machines/MachineErrorChecker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cura/Machines/MachineErrorChecker.py b/cura/Machines/MachineErrorChecker.py index 5edee0778f..87d50e46d4 100644 --- a/cura/Machines/MachineErrorChecker.py +++ b/cura/Machines/MachineErrorChecker.py @@ -61,6 +61,7 @@ class MachineErrorChecker(QObject): self._machine_manager.globalContainerChanged.connect(self.startErrorCheck) self._onMachineChanged() + self.startErrorCheck() def _setCheckTimer(self) -> None: """A QTimer to regulate error check frequency From da0509cda35f66df6f03bb1046ff6170a7ab5c4a Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 30 Jul 2025 15:58:26 +0200 Subject: [PATCH 135/159] Fix painting through an invisible object CURA-12660 --- cura/PickingPass.py | 6 ++++-- plugins/PaintTool/PaintTool.py | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cura/PickingPass.py b/cura/PickingPass.py index 4d6ef671df..dd91659afe 100644 --- a/cura/PickingPass.py +++ b/cura/PickingPass.py @@ -7,6 +7,7 @@ from UM.Qt.QtApplication import QtApplication from UM.Logger import Logger from UM.Math.Vector import Vector from UM.Resources import Resources +from UM.Scene.Selection import Selection from UM.View.RenderPass import RenderPass from UM.View.GL.OpenGL import OpenGL @@ -27,13 +28,14 @@ class PickingPass(RenderPass): .. note:: that in order to increase precision, the 24 bit depth value is encoded into all three of the R,G & B channels """ - def __init__(self, width: int, height: int) -> None: + def __init__(self, width: int, height: int, only_selected_objects: bool = False) -> None: super().__init__("picking", width, height) self._renderer = QtApplication.getInstance().getRenderer() self._shader = None #type: Optional[ShaderProgram] self._scene = QtApplication.getInstance().getController().getScene() + self._only_selected_objects = only_selected_objects def render(self) -> None: if not self._shader: @@ -53,7 +55,7 @@ class PickingPass(RenderPass): # Fill up the batch with objects that can be sliced. ` for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. - if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible(): + if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible() and (not self._only_selected_objects or Selection.isSelected(node)): batch.addItem(node.getWorldTransformation(copy = False), node.getMeshData(), normal_transformation=node.getCachedNormalMatrix()) self.bind() diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index 524011af9d..fcc5c9a3a0 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -300,7 +300,9 @@ class PaintTool(Tool): return False if not self._picking_pass: - self._picking_pass = PickingPass(camera.getViewportWidth(), camera.getViewportHeight()) + self._picking_pass = PickingPass(camera.getViewportWidth(), + camera.getViewportHeight(), + only_selected_objects = True) self._picking_pass.render() self._selection_pass.renderFacesMode() From 73f5b817b438df0b03b943b3993d2dea656cdffe Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 30 Jul 2025 15:59:30 +0200 Subject: [PATCH 136/159] Display build plate in paint mode CURA-12660 --- plugins/PaintTool/PaintView.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index 22eb8c55f6..18dc067c5a 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -8,10 +8,13 @@ from typing import Optional, List, Tuple, Dict from PyQt6.QtGui import QImage, QColor, QPainter from cura.CuraApplication import CuraApplication +from cura.BuildVolume import BuildVolume 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.View.SelectionPass import SelectionPass +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 @@ -163,6 +166,11 @@ class PaintView(View): def beginRendering(self) -> None: renderer = self.getRenderer() self._checkSetup() + + 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) From 6bf9a8a0aeba3b7157ae4bdd695e9ffa50396bca Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 30 Jul 2025 16:00:13 +0200 Subject: [PATCH 137/159] Ignore invisible object for selection in paint mode CURA-12660 --- plugins/PaintTool/PaintView.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index 18dc067c5a..c37afd178d 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -3,7 +3,7 @@ import os from PyQt6.QtCore import QRect -from typing import Optional, List, Tuple, Dict +from typing import Optional, List, Tuple, Dict, cast from PyQt6.QtGui import QImage, QColor, QPainter @@ -47,7 +47,9 @@ class PaintView(View): self._force_opaque_mask = QImage(2, 2, QImage.Format.Format_Mono) self._force_opaque_mask.fill(1) - CuraApplication.getInstance().engineCreatedSignal.connect(self._makePaintModes) + application = CuraApplication.getInstance() + application.engineCreatedSignal.connect(self._makePaintModes) + self._scene = application.getController().getScene() def _makePaintModes(self): theme = CuraApplication.getInstance().getTheme() @@ -164,6 +166,9 @@ class PaintView(View): return start_index, end_index def beginRendering(self) -> None: + if self._current_paint_type == "": + return + renderer = self.getRenderer() self._checkSetup() @@ -174,12 +179,20 @@ class PaintView(View): paint_batch = renderer.createRenderBatch(shader=self._paint_shader) renderer.addRenderBatch(paint_batch) - node = Selection.getSelectedObject(0) - if node is None: - return + display_objects = Selection.getAllSelectedObjects().copy() + if display_objects: + selection_pass = cast(SelectionPass, renderer.getRenderPass("selection")) + if selection_pass is not None: + selection_pass.setIgnoreUnselectedObjectsDuringNextRender() + else: + for node in DepthFirstIterator(self._scene.getRoot()): + if node.callDecoration("isSliceable"): + display_objects.append(node) - if self._current_paint_type == "": - return + for node in display_objects: + 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]) @@ -187,8 +200,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()) From 6896c0ed4bfb23475fb52d50d76900ec6a85906e Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 30 Jul 2025 16:53:41 +0200 Subject: [PATCH 138/159] Display classic view when there is no selection CURA-12660 --- plugins/PaintTool/PaintView.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index c37afd178d..60656086ff 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -9,10 +9,10 @@ from PyQt6.QtGui import QImage, QColor, QPainter from cura.CuraApplication import CuraApplication from cura.BuildVolume import BuildVolume +from plugins.SolidView.SolidView import SolidView 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.View.SelectionPass import SelectionPass from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Selection import Selection @@ -23,7 +23,7 @@ from UM.Math.Color import Color catalog = i18nCatalog("cura") -class PaintView(View): +class PaintView(SolidView): """View for model-painting.""" UNDO_STACK_SIZE = 1024 @@ -62,6 +62,8 @@ class PaintView(View): } def _checkSetup(self): + super()._checkSetup() + if not self._paint_shader: shader_filename = os.path.join(PluginRegistry.getInstance().getPluginPath("PaintTool"), "paint.shader") self._paint_shader = OpenGL.getInstance().createShaderProgram(shader_filename) @@ -169,8 +171,14 @@ class PaintView(View): if self._current_paint_type == "": return - renderer = self.getRenderer() + display_objects = Selection.getAllSelectedObjects().copy() + if not display_objects: + # Display the classic view until an object is selected + super().beginRendering() + return + self._checkSetup() + renderer = self.getRenderer() for node in DepthFirstIterator(self._scene.getRoot()): if isinstance(node, BuildVolume): @@ -179,15 +187,9 @@ class PaintView(View): paint_batch = renderer.createRenderBatch(shader=self._paint_shader) renderer.addRenderBatch(paint_batch) - display_objects = Selection.getAllSelectedObjects().copy() - if display_objects: - selection_pass = cast(SelectionPass, renderer.getRenderPass("selection")) - if selection_pass is not None: - selection_pass.setIgnoreUnselectedObjectsDuringNextRender() - else: - for node in DepthFirstIterator(self._scene.getRoot()): - if node.callDecoration("isSliceable"): - display_objects.append(node) + selection_pass = cast(SelectionPass, renderer.getRenderPass("selection")) + if selection_pass is not None: + selection_pass.setIgnoreUnselectedObjectsDuringNextRender() for node in display_objects: paint_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData(), normal_transformation=node.getCachedNormalMatrix()) From 91e986697d229f85fd0375424d43e57e32ce5685 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 30 Jul 2025 16:54:05 +0200 Subject: [PATCH 139/159] Fix painting after changing the selected object CURA-12660 --- plugins/PaintTool/PaintTool.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index fcc5c9a3a0..2099a59691 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -293,6 +293,7 @@ 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() From 6292f5b133f6423bd758bf1d56c738f8517d050d Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 30 Jul 2025 16:57:29 +0200 Subject: [PATCH 140/159] Hide paint-on-support option until it is implemented CURA-12660 --- plugins/PaintTool/PaintTool.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml index 4cbe9d4ade..e3f244dd4c 100644 --- a/plugins/PaintTool/PaintTool.qml +++ b/plugins/PaintTool/PaintTool.qml @@ -57,6 +57,7 @@ Item icon: "Support" tooltipText: catalog.i18nc("@tooltip", "Refine support placement by defining preferred/avoidance areas") mode: "support" + visible: false } } From ef7bde87fa4469174fd01c5cfca8cca59c545129 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 31 Jul 2025 11:24:36 +0200 Subject: [PATCH 141/159] Allow painting only when 1 object is selected CURA-12660 --- plugins/PaintTool/PaintView.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index 60656086ff..61a6d0079c 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -172,8 +172,8 @@ class PaintView(SolidView): return display_objects = Selection.getAllSelectedObjects().copy() - if not display_objects: - # Display the classic view until an object is selected + if len(display_objects) != 1: + # Display the classic view until a single object is selected super().beginRendering() return From 3cb7eb3c873ed195ae998b0dd8ed15cea884a162 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 31 Jul 2025 11:47:41 +0200 Subject: [PATCH 142/159] Avoid too dark or too light areas while painting CURA-12660 This avoid having parts of the model where you cannot see the painted areas anymore --- plugins/PaintTool/paint.shader | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/PaintTool/paint.shader b/plugins/PaintTool/paint.shader index bd769f5cb2..f2af66ffe6 100644 --- a/plugins/PaintTool/paint.shader +++ b/plugins/PaintTool/paint.shader @@ -55,7 +55,7 @@ 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; @@ -122,7 +122,7 @@ 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; From 347f4d10ca1eb6a4c5f010588a840745660102aa Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 31 Jul 2025 13:22:16 +0200 Subject: [PATCH 143/159] Apply proper AppImage compression level --- packaging/AppImage-builder/AppImageBuilder.yml.jinja | 1 + 1 file changed, 1 insertion(+) diff --git a/packaging/AppImage-builder/AppImageBuilder.yml.jinja b/packaging/AppImage-builder/AppImageBuilder.yml.jinja index c6e7a7123a..29144b0e77 100644 --- a/packaging/AppImage-builder/AppImageBuilder.yml.jinja +++ b/packaging/AppImage-builder/AppImageBuilder.yml.jinja @@ -77,3 +77,4 @@ AppImage: arch: {{ arch }} file_name: {{ file_name }} update-information: guess + comp: gzip From ab58dec5d141c12a0cde128fb702126ec39914cd Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 1 Aug 2025 13:10:03 +0200 Subject: [PATCH 144/159] Fix unability to paint with visible message box CURA-12660 When a message box is displayed, some offscreen rendering passes (face selection) render an unpredictable result and we are unable to start painting. This went through a refactoring of the rendering passes. Since doing the offscreen rendering outside the Qt rendering loop caused some troubles, we now use the rendering passes only inside the Qt rendering loop, so that they work properly. Tools also have the ability to indicate which extra passes they require, so that we don't run all the passes when they are not required. Since this issue also concerns the support blockers placement and rotation by face selection, they have been updated so that they now also always work. The face selection mechanism using the Selection class was partially working and used only by the rotation, so now it has been deprecated in favor of the new mechanism. --- cura/CuraApplication.py | 4 +++ cura/CuraRenderer.py | 46 ++++++++++++++++++++++++++ cura/PickingPass.py | 2 +- plugins/PaintTool/PaintTool.py | 40 +++++++++++++++------- plugins/PaintTool/PaintView.py | 5 --- plugins/SupportEraser/SupportEraser.py | 17 +++++++--- 6 files changed, 91 insertions(+), 23 deletions(-) create mode 100644 cura/CuraRenderer.py diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 8af98c2d0e..660f312468 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -60,6 +60,7 @@ from cura import ApplicationMetadata from cura.API import CuraAPI from cura.API.Account import Account from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob +from cura.CuraRenderer import CuraRenderer from cura.Machines.MachineErrorChecker import MachineErrorChecker from cura.Machines.Models.BuildPlateModel import BuildPlateModel from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel @@ -362,6 +363,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) diff --git a/cura/CuraRenderer.py b/cura/CuraRenderer.py new file mode 100644 index 0000000000..77030b3fe8 --- /dev/null +++ b/cura/CuraRenderer.py @@ -0,0 +1,46 @@ +# Copyright (c) 2025 UltiMaker +# Uranium is released under the terms of the LGPLv3 or higher. + + +from typing import TYPE_CHECKING + +from cura.PickingPass import PickingPass +from UM.Qt.QtRenderer import QtRenderer +from UM.View.RenderPass import RenderPass +from UM.View.SelectionPass import SelectionPass + +if TYPE_CHECKING: + from cura.CuraApplication import CuraApplication + + +class CuraRenderer(QtRenderer): + """An overridden Renderer implementation that adds some behaviors specific to Cura.""" + + def __init__(self, application: "CuraApplication") -> None: + super().__init__() + + self._controller = application.getController() + self._controller.activeToolChanged.connect(self._onActiveToolChanged) + self._extra_rendering_passes: list[RenderPass] = [] + + def _onActiveToolChanged(self) -> None: + tool_extra_rendering_passes = [] + + active_tool = self._controller.getActiveTool() + if active_tool is not None: + tool_extra_rendering_passes = active_tool.getRequiredExtraRenderingPasses() + + for extra_rendering_pass in self._extra_rendering_passes: + extra_rendering_pass.setEnabled(extra_rendering_pass.getName() in tool_extra_rendering_passes) + + def _makeRenderPasses(self) -> list[RenderPass]: + self._extra_rendering_passes = [ + SelectionPass(self._viewport_width, self._viewport_height, SelectionPass.SelectionMode.FACES), + PickingPass(self._viewport_width, self._viewport_height, only_selected_objects=True), + PickingPass(self._viewport_width, self._viewport_height, only_selected_objects=False) + ] + + for extra_rendering_pass in self._extra_rendering_passes: + extra_rendering_pass.setEnabled(False) + + return super()._makeRenderPasses() + self._extra_rendering_passes diff --git a/cura/PickingPass.py b/cura/PickingPass.py index dd91659afe..e585e72269 100644 --- a/cura/PickingPass.py +++ b/cura/PickingPass.py @@ -29,7 +29,7 @@ class PickingPass(RenderPass): """ def __init__(self, width: int, height: int, only_selected_objects: bool = False) -> None: - super().__init__("picking", width, height) + super().__init__("picking" if not only_selected_objects else "picking_selected", width, height) self._renderer = QtApplication.getInstance().getRenderer() diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index 2099a59691..e57c6d5a11 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -17,7 +17,9 @@ from UM.Scene.SceneNode import SceneNode from UM.Scene.Selection import Selection from UM.Tool import Tool +from cura.CuraApplication import CuraApplication from cura.PickingPass import PickingPass +from UM.View.SelectionPass import SelectionPass from .PaintView import PaintView @@ -34,6 +36,7 @@ class PaintTool(Tool): super().__init__() self._picking_pass: Optional[PickingPass] = None + self._faces_selection_pass: Optional[SelectionPass] = None self._shortcut_key: Qt.Key = Qt.Key.Key_P @@ -52,6 +55,8 @@ class PaintTool(Tool): self._last_mouse_coords: Optional[Tuple[int, int]] = None self._last_face_id: Optional[int] = None + Selection.selectionChanged.connect(self._updateIgnoreUnselectedObjects) + def _createBrushPen(self) -> QPen: pen = QPen() pen.setWidth(self._brush_size) @@ -179,7 +184,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 @@ -248,11 +253,14 @@ class PaintTool(Tool): if event.type == Event.ToolActivateEvent: controller.setActiveStage("PrepareStage") controller.setActiveView("PaintTool") # Because that's the plugin-name, and the view is registered to it. + self._updateIgnoreUnselectedObjects() return True if event.type == Event.ToolDeactivateEvent: controller.setActiveStage("PrepareStage") controller.setActiveView("SolidView") + CuraApplication.getInstance().getRenderer().getRenderPass("selection").setIgnoreUnselectedObjects(False) + CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces").setIgnoreUnselectedObjects(False) return True if event.type == Event.MouseReleaseEvent and self._controller.getToolsEnabled(): @@ -281,8 +289,15 @@ class PaintTool(Tool): if paintview is None: return False - if not self._selection_pass: - 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._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: @@ -300,14 +315,6 @@ class PaintTool(Tool): if not self._mesh_transformed_cache: return False - if not self._picking_pass: - self._picking_pass = PickingPass(camera.getViewportWidth(), - camera.getViewportHeight(), - only_selected_objects = True) - 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 @@ -347,4 +354,13 @@ class PaintTool(Tool): if node is None: node = Selection.getSelectedObject(0) if node is not None: - Application.getInstance().getController().getScene().sceneChanged.emit(node) \ No newline at end of file + Application.getInstance().getController().getScene().sceneChanged.emit(node) + + def getRequiredExtraRenderingPasses(self) -> list[str]: + return ["selection_faces", "picking_selected"] + + def _updateIgnoreUnselectedObjects(self): + if self._controller.getActiveTool() is self: + ignore_unselected_objects = len(Selection.getAllSelectedObjects()) == 1 + CuraApplication.getInstance().getRenderer().getRenderPass("selection").setIgnoreUnselectedObjects(ignore_unselected_objects) + CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces").setIgnoreUnselectedObjects(ignore_unselected_objects) \ No newline at end of file diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index 61a6d0079c..a3d4b36315 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -13,7 +13,6 @@ from plugins.SolidView.SolidView import SolidView from UM.PluginRegistry import PluginRegistry from UM.View.GL.ShaderProgram import ShaderProgram from UM.View.GL.Texture import Texture -from UM.View.SelectionPass import SelectionPass from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Selection import Selection from UM.View.GL.OpenGL import OpenGL @@ -187,10 +186,6 @@ class PaintView(SolidView): paint_batch = renderer.createRenderBatch(shader=self._paint_shader) renderer.addRenderBatch(paint_batch) - selection_pass = cast(SelectionPass, renderer.getRenderPass("selection")) - if selection_pass is not None: - selection_pass.setIgnoreUnselectedObjectsDuringNextRender() - for node in display_objects: paint_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData(), normal_transformation=node.getCachedNormalMatrix()) self._current_paint_texture = node.callDecoration("getPaintTexture") diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 0a714396aa..afdad6a4d0 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -1,6 +1,8 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import Optional + from PyQt6.QtCore import Qt, QTimer from PyQt6.QtWidgets import QApplication @@ -35,6 +37,7 @@ class SupportEraser(Tool): self._controller = self.getController() self._selection_pass = None + self._picking_pass: Optional[PickingPass] = None CuraApplication.getInstance().globalContainerStackChanged.connect(self._updateEnabled) # Note: if the selection is cleared with this tool active, there is no way to switch to @@ -84,12 +87,13 @@ class SupportEraser(Tool): # Only "normal" meshes can have anti_overhang_meshes added to them return - # Create a pass for picking a world-space location from the mouse location - active_camera = self._controller.getScene().getActiveCamera() - picking_pass = PickingPass(active_camera.getViewportWidth(), active_camera.getViewportHeight()) - picking_pass.render() + # Get the pass for picking a world-space location from the mouse location + if self._picking_pass is None: + self._picking_pass = Application.getInstance().getRenderer().getRenderPass("picking_selected") + if not self._picking_pass: + return - picked_position = picking_pass.getPickedPosition(event.x, event.y) + picked_position = self._picking_pass.getPickedPosition(event.x, event.y) # Add the anti_overhang_mesh cube at the picked location self._createEraserMesh(picked_node, picked_position) @@ -189,3 +193,6 @@ class SupportEraser(Tool): mesh.calculateNormals() return mesh + + def getRequiredExtraRenderingPasses(self) -> list[str]: + return ["picking_selected"] \ No newline at end of file From ea488f02029563c587abdc45bcb30d56377a1ef4 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 1 Aug 2025 14:23:02 +0200 Subject: [PATCH 145/159] Fix wrongly displayed error message CURA-12660 --- cura/XRayPass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/XRayPass.py b/cura/XRayPass.py index 965294ba89..20fe38741e 100644 --- a/cura/XRayPass.py +++ b/cura/XRayPass.py @@ -16,7 +16,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator class XRayPass(RenderPass): def __init__(self, width, height): - super().__init__("xray", width, height) + super().__init__("xray", width, height, -100) self._shader = None self._gl = OpenGL.getInstance().getBindingsObject() From 78daa94ebff6a76b5fa91e59483c6541a9c80a18 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 1 Aug 2025 17:08:53 +0200 Subject: [PATCH 146/159] Save and restore painting tool settings CURA-12660 --- plugins/PaintTool/BrushColorButton.qml | 22 ++++++++++--- plugins/PaintTool/BrushShapeButton.qml | 22 ++++++++++--- plugins/PaintTool/PaintModeButton.qml | 23 +++++++++++--- plugins/PaintTool/PaintTool.py | 43 ++++++++++++++++++++------ plugins/PaintTool/PaintTool.qml | 20 ++++-------- plugins/PaintTool/PaintView.py | 22 +++++++++---- plugins/SolidView/SolidView.py | 5 +-- 7 files changed, 113 insertions(+), 44 deletions(-) diff --git a/plugins/PaintTool/BrushColorButton.qml b/plugins/PaintTool/BrushColorButton.qml index 71556f2681..ae4ab6243f 100644 --- a/plugins/PaintTool/BrushColorButton.qml +++ b/plugins/PaintTool/BrushColorButton.qml @@ -13,13 +13,27 @@ UM.ToolbarButton property string color - checked: base.selectedColor === buttonBrushColor.color - onClicked: setColor() function setColor() { - base.selectedColor = buttonBrushColor.color - UM.Controller.triggerActionWithData("setBrushColor", buttonBrushColor.color) + UM.Controller.setProperty("BrushColor", buttonBrushColor.color); + } + + function isChecked() + { + return UM.Controller.properties.getValue("BrushColor") === buttonBrushColor.color; + } + + Component.onCompleted: + { + buttonBrushColor.checked = isChecked(); + } + + Binding + { + target: buttonBrushColor + property: "checked" + value: isChecked() } } diff --git a/plugins/PaintTool/BrushShapeButton.qml b/plugins/PaintTool/BrushShapeButton.qml index 5c290e4a13..ef4256792a 100644 --- a/plugins/PaintTool/BrushShapeButton.qml +++ b/plugins/PaintTool/BrushShapeButton.qml @@ -13,13 +13,27 @@ UM.ToolbarButton property int shape - checked: base.selectedShape === buttonBrushShape.shape - onClicked: setShape() function setShape() { - base.selectedShape = buttonBrushShape.shape - UM.Controller.triggerActionWithData("setBrushShape", buttonBrushShape.shape) + UM.Controller.setProperty("BrushShape", buttonBrushShape.shape) + } + + function isChecked() + { + return UM.Controller.properties.getValue("BrushShape") === buttonBrushShape.shape; + } + + Component.onCompleted: + { + buttonBrushShape.checked = isChecked(); + } + + Binding + { + target: buttonBrushShape + property: "checked" + value: isChecked() } } diff --git a/plugins/PaintTool/PaintModeButton.qml b/plugins/PaintTool/PaintModeButton.qml index 473996e04b..eb294f7ad6 100644 --- a/plugins/PaintTool/PaintModeButton.qml +++ b/plugins/PaintTool/PaintModeButton.qml @@ -6,19 +6,34 @@ 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) + UM.Controller.setProperty("PaintType", modeSelectorButton.mode); + } + + function isSelected() + { + return UM.Controller.properties.getValue("PaintType") === modeSelectorButton.mode; + } + + Component.onCompleted: + { + modeSelectorButton.selected = isSelected(); + } + + Binding + { + target: modeSelectorButton + property: "selected" + value: isSelected() } } diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index e57c6d5a11..a1812a5766 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -45,8 +45,8 @@ class PaintTool(Tool): 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_color: str = "preferred" + self._brush_shape: PaintTool.Brush.Shape = PaintTool.Brush.Shape.CIRCLE self._brush_pen: QPen = self._createBrushPen() self._mouse_held: bool = False @@ -55,6 +55,8 @@ class PaintTool(Tool): self._last_mouse_coords: Optional[Tuple[int, int]] = None self._last_face_id: Optional[int] = None + self.setExposedProperties("PaintType", "BrushSize", "BrushColor", "BrushShape") + Selection.selectionChanged.connect(self._updateIgnoreUnselectedObjects) def _createBrushPen(self) -> QPen: @@ -91,28 +93,51 @@ class PaintTool(Tool): return stroke_image, (start_x, start_y) + def getPaintType(self) -> str: + paint_view = self._get_paint_view() + if paint_view is None: + return "" + + return paint_view.getPaintType() + def setPaintType(self, paint_type: str) -> None: paint_view = self._get_paint_view() if paint_view is None: return - paint_view.setPaintType(paint_type) + if paint_type != self.getPaintType(): + paint_view.setPaintType(paint_type) - self._brush_pen = self._createBrushPen() - self._updateScene() + self._brush_pen = self._createBrushPen() + self._updateScene() + self.propertyChanged.emit() + + def getBrushSize(self) -> int: + return self._brush_size def setBrushSize(self, brush_size: float) -> None: - 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() @@ -251,13 +276,11 @@ 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. self._updateIgnoreUnselectedObjects() return True if event.type == Event.ToolDeactivateEvent: - controller.setActiveStage("PrepareStage") controller.setActiveView("SolidView") CuraApplication.getInstance().getRenderer().getRenderPass("selection").setIgnoreUnselectedObjects(False) CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces").setIgnoreUnselectedObjects(False) diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml index e3f244dd4c..94642b1f66 100644 --- a/plugins/PaintTool/PaintTool.qml +++ b/plugins/PaintTool/PaintTool.qml @@ -15,10 +15,6 @@ Item height: childrenRect.height UM.I18nCatalog { id: catalog; name: "cura"} - property string selectedMode: "" - property string selectedColor: "" - property int selectedShape: 0 - Action { id: undoAction @@ -167,15 +163,19 @@ Item from: 1 to: 40 - value: 10 onPressedChanged: function(pressed) { if(! pressed) { - UM.Controller.triggerActionWithData("setBrushSize", shapeSizeSlider.value) + UM.Controller.setProperty("BrushSize", shapeSizeSlider.value); } } + + Component.onCompleted: + { + shapeSizeSlider.value = UM.Controller.properties.getValue("BrushSize"); + } } //Line between the sections. @@ -227,12 +227,4 @@ Item } } } - - Component.onCompleted: - { - // Force first types for consistency, otherwise UI may become different from controller - rowPaintMode.children[0].setMode() - rowBrushColor.children[1].setColor() - rowBrushShape.children[1].setShape() - } } diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index a3d4b36315..c723f4321d 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -60,6 +60,8 @@ class PaintView(SolidView): "support": usual_types, } + self._current_paint_type = "seam" + def _checkSetup(self): super()._checkSetup() @@ -82,6 +84,8 @@ class PaintView(SolidView): if self._current_paint_texture is None or self._current_paint_texture.getImage() is None: return + self._prepareDataMapping() + actual_image = self._current_paint_texture.getImage() bit_range_start, bit_range_end = self._current_bits_ranges @@ -141,20 +145,26 @@ class PaintView(SolidView): 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 + self._prepareDataMapping() + + 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) - 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]: @@ -167,7 +177,7 @@ class PaintView(SolidView): return start_index, end_index def beginRendering(self) -> None: - if self._current_paint_type == "": + if self._current_paint_type not in self._paint_modes: return display_objects = Selection.getAllSelectedObjects().copy() diff --git a/plugins/SolidView/SolidView.py b/plugins/SolidView/SolidView.py index bffc3aa526..e25273cb13 100644 --- a/plugins/SolidView/SolidView.py +++ b/plugins/SolidView/SolidView.py @@ -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() From e742ca81f384925473fd219c14f093fd32c86db9 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 4 Aug 2025 10:24:29 +0200 Subject: [PATCH 147/159] Fixed more possible crashes CURA-12528 --- plugins/PaintTool/PaintView.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index c2562fa36a..749fa463e4 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -150,7 +150,9 @@ class PaintView(View): mesh = node.getMeshData() if not mesh.hasUVCoordinates(): texture_width, texture_height = mesh.calculateUnwrappedUVCoordinates() - node.callDecoration("prepareTexture", texture_width, texture_height) + 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) From 44d6c0a9694d6cb5ddbf78da25735e37a0e8c7e2 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 4 Aug 2025 11:28:31 +0200 Subject: [PATCH 148/159] Call SolidView dynamically instead of by inheritance CURA-12660 The previous method actually doesn't work when Cura is packaged because the plugins paths change. This method is much safer, and uses the actual SolidView instance. --- plugins/PaintTool/PaintView.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index c723f4321d..e4a8b3c493 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -9,8 +9,8 @@ from PyQt6.QtGui import QImage, QColor, QPainter from cura.CuraApplication import CuraApplication from cura.BuildVolume import BuildVolume -from plugins.SolidView.SolidView import SolidView from UM.PluginRegistry import PluginRegistry +from UM.View.View import View from UM.View.GL.ShaderProgram import ShaderProgram from UM.View.GL.Texture import Texture from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator @@ -22,7 +22,7 @@ from UM.Math.Color import Color catalog = i18nCatalog("cura") -class PaintView(SolidView): +class PaintView(View): """View for model-painting.""" UNDO_STACK_SIZE = 1024 @@ -50,6 +50,8 @@ class PaintView(SolidView): application.engineCreatedSignal.connect(self._makePaintModes) self._scene = application.getController().getScene() + self._solid_view = None + def _makePaintModes(self): theme = CuraApplication.getInstance().getTheme() usual_types = {"none": self.PaintType(Color(*theme.getColor("paint_normal_area").getRgb()), 0), @@ -63,8 +65,6 @@ class PaintView(SolidView): self._current_paint_type = "seam" def _checkSetup(self): - super()._checkSetup() - if not self._paint_shader: shader_filename = os.path.join(PluginRegistry.getInstance().getPluginPath("PaintTool"), "paint.shader") self._paint_shader = OpenGL.getInstance().createShaderProgram(shader_filename) @@ -180,10 +180,16 @@ class PaintView(SolidView): if self._current_paint_type not in self._paint_modes: return + if self._solid_view is None: + plugin_registry = PluginRegistry.getInstance() + solid_view = plugin_registry.getPluginObject("SolidView") + if isinstance(solid_view, View): + self._solid_view = solid_view + display_objects = Selection.getAllSelectedObjects().copy() - if len(display_objects) != 1: + if len(display_objects) != 1 and self._solid_view is not None: # Display the classic view until a single object is selected - super().beginRendering() + self._solid_view.beginRendering() return self._checkSetup() From 968576472121c88735e0ebc5252336719738fac2 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 4 Aug 2025 15:19:04 +0200 Subject: [PATCH 149/159] Implement undo-redo by full stroke CURA-12661 --- plugins/PaintTool/PaintTool.py | 4 +- plugins/PaintTool/PaintUndoCommand.py | 104 ++++++++++++++++++++++++++ plugins/PaintTool/PaintView.py | 95 ++++++++--------------- 3 files changed, 136 insertions(+), 67 deletions(-) create mode 100644 plugins/PaintTool/PaintUndoCommand.py diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index fa6436f10d..4f6516ffd7 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -130,7 +130,7 @@ class PaintTool(Tool): width, height = paintview.getUvTexDimensions() clear_image = QImage(width, height, QImage.Format.Format_RGB32) clear_image.fill(Qt.GlobalColor.white) - paintview.addStroke(clear_image, 0, 0, "none") + paintview.addStroke(clear_image, 0, 0, "none", False) self._updateScene() @@ -333,7 +333,7 @@ class PaintTool(Tool): end_coords[0] * w, end_coords[1] * h ) - paintview.addStroke(sub_image, start_x, start_y, self._brush_color) + paintview.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) diff --git a/plugins/PaintTool/PaintUndoCommand.py b/plugins/PaintTool/PaintUndoCommand.py new file mode 100644 index 0000000000..c100780c7f --- /dev/null +++ b/plugins/PaintTool/PaintUndoCommand.py @@ -0,0 +1,104 @@ +# Copyright (c) 2025 UltiMaker +# Cura is released under the terms of the LGPLv3 or higher. + +from typing import cast, Optional + +from PyQt6.QtCore import QRect, QPoint +from PyQt6.QtGui import QUndoCommand, QImage, QPainter + +from UM.View.GL.Texture import Texture + + +class PaintUndoCommand(QUndoCommand): + """Provides the command that does the actual painting on objects with undo/redo mechanisms""" + + def __init__(self, + texture: Texture, + stroke_mask: QImage, + x: int, + y: int, + set_value: int, + bit_range: tuple[int, int], + mergeable: bool) -> None: + super().__init__() + + self._original_texture_image: Optional[QImage] = texture.getImage().copy() if not mergeable else None + self._texture: Texture = texture + self._stroke_mask: QImage = stroke_mask + self._x: int = x + self._y: int = y + self._set_value: int = set_value + self._bit_range: tuple[int, int] = bit_range + self._mergeable: bool = mergeable + + def id(self) -> int: + # Since the undo stack will contain only commands of this type, we can use a fixed ID + return 0 + + def redo(self) -> None: + actual_image = self._texture.getImage() + + bit_range_start, bit_range_end = self._bit_range + full_int32 = 0xffffffff + clear_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_mask) + painter.end() + + set_value_image = self._stroke_mask.copy() + painter = QPainter(set_value_image) + painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Multiply) + painter.fillRect(image_rect, self._set_value) + painter.end() + + stroked_image = actual_image.copy(self._x, self._y, self._stroke_mask.width(), self._stroke_mask.height()) + painter = QPainter(stroked_image) + painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceAndDestination) + painter.drawImage(0, 0, clear_bits_image) + painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceOrDestination) + painter.drawImage(0, 0, set_value_image) + painter.end() + + self._texture.setSubImage(stroked_image, self._x, self._y) + + def undo(self) -> None: + if self._original_texture_image is not None: + self._texture.setSubImage(self._original_texture_image.copy(self._x, + self._y, + self._stroke_mask.width(), + self._stroke_mask.height()), + self._x, + self._y) + + def mergeWith(self, command: QUndoCommand) -> bool: + if not isinstance(command, PaintUndoCommand): + return False + paint_undo_command = cast(PaintUndoCommand, command) + + if not paint_undo_command._mergeable: + return False + + self_rect = QRect(QPoint(self._x, self._y), self._stroke_mask.size()) + command_rect = QRect(QPoint(paint_undo_command._x, paint_undo_command._y), paint_undo_command._stroke_mask.size()) + bounding_rect = self_rect.united(command_rect) + + merged_mask = QImage(bounding_rect.width(), bounding_rect.height(), self._stroke_mask.format()) + merged_mask.fill(0) + + painter = QPainter(merged_mask) + painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Lighten) + painter.drawImage(self._x - bounding_rect.x(), self._y - bounding_rect.y(), self._stroke_mask) + painter.drawImage(paint_undo_command._x - bounding_rect.x(), paint_undo_command._y - bounding_rect.y(), paint_undo_command._stroke_mask) + painter.end() + + self._x = bounding_rect.x() + self._y = bounding_rect.y() + self._stroke_mask = merged_mask + + return True diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index 749fa463e4..da00e16c2a 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -5,7 +5,7 @@ import os from PyQt6.QtCore import QRect from typing import Optional, List, Tuple, Dict -from PyQt6.QtGui import QImage, QColor, QPainter +from PyQt6.QtGui import QImage, QColor, QPainter, QUndoStack from cura.CuraApplication import CuraApplication from UM.PluginRegistry import PluginRegistry @@ -17,14 +17,14 @@ 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): """View for model-painting.""" - UNDO_STACK_SIZE = 1024 - class PaintType: def __init__(self, display_color: Color, value: int): self.display_color: Color = display_color @@ -38,8 +38,8 @@ class PaintView(View): 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._force_opaque_mask = QImage(2, 2, QImage.Format.Format_Mono) self._force_opaque_mask.fill(1) @@ -61,74 +61,39 @@ class PaintView(View): 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() + 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: From 586c2939a60dd2750c44bdad383e5d66e3a82d0a Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 4 Aug 2025 15:46:51 +0200 Subject: [PATCH 150/159] Enable undo/redo buttons when appropriate CURA-12661 --- plugins/PaintTool/PaintTool.py | 68 ++++++++++++++------------------- plugins/PaintTool/PaintTool.qml | 20 +++++++++- plugins/PaintTool/PaintView.py | 20 +++++++--- plugins/PaintTool/__init__.py | 7 +++- 4 files changed, 65 insertions(+), 50 deletions(-) diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index b9f7a5e95b..293c069786 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -33,9 +33,13 @@ class PaintTool(Tool): SQUARE = 0 CIRCLE = 1 - def __init__(self) -> None: + 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 @@ -56,7 +60,7 @@ class PaintTool(Tool): self._last_mouse_coords: Optional[Tuple[int, int]] = None self._last_face_id: Optional[int] = None - self.setExposedProperties("PaintType", "BrushSize", "BrushColor", "BrushShape") + self.setExposedProperties("PaintType", "BrushSize", "BrushColor", "BrushShape", "CanUndo", "CanRedo") Selection.selectionChanged.connect(self._updateIgnoreUnselectedObjects) @@ -95,19 +99,11 @@ class PaintTool(Tool): return stroke_image, (start_x, start_y) def getPaintType(self) -> str: - paint_view = self._get_paint_view() - if paint_view is None: - return "" - - return paint_view.getPaintType() + 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(): - paint_view.setPaintType(paint_type) + self._view.setPaintType(paint_type) self._brush_pen = self._createBrushPen() self._updateScene() @@ -140,38 +136,34 @@ class PaintTool(Tool): 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 _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", False) + 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 @@ -330,10 +322,6 @@ 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: @@ -379,7 +367,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, @@ -387,7 +375,7 @@ class PaintTool(Tool): end_coords[0] * w, end_coords[1] * h ) - paintview.addStroke(sub_image, start_x, start_y, self._brush_color, is_moved) + 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) diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml index 2bb3106dd5..c448835bc5 100644 --- a/plugins/PaintTool/PaintTool.qml +++ b/plugins/PaintTool/PaintTool.qml @@ -19,14 +19,14 @@ Item { id: undoAction shortcut: "Ctrl+L" - onTriggered: UM.Controller.triggerActionWithData("undoStackAction", false) + onTriggered: UM.Controller.triggerAction("undoStackAction") } Action { id: redoAction shortcut: "Ctrl+Shift+L" - onTriggered: UM.Controller.triggerActionWithData("undoStackAction", true) + onTriggered: UM.Controller.triggerAction("redoStackAction") } Column @@ -192,6 +192,7 @@ Item { id: undoButton + enabled: undoAction.enabled text: catalog.i18nc("@action:button", "Undo Stroke") toolItem: UM.ColorImage { @@ -206,6 +207,7 @@ Item { id: redoButton + enabled: redoAction.enabled text: catalog.i18nc("@action:button", "Redo Stroke") toolItem: UM.ColorImage { @@ -227,4 +229,18 @@ Item } } } + + Binding + { + target: undoAction + property: "enabled" + value: UM.Controller.properties.getValue("CanUndo") + } + + Binding + { + target: redoAction + property: "enabled" + value: UM.Controller.properties.getValue("CanRedo") + } } diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index ed06da17d4..b5cd44772d 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -2,10 +2,10 @@ # 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, cast +from PyQt6.QtCore import QRect, pyqtSignal +from typing import Optional, Dict -from PyQt6.QtGui import QImage, QColor, QPainter, QUndoStack +from PyQt6.QtGui import QImage, QUndoStack from cura.CuraApplication import CuraApplication from cura.BuildVolume import BuildVolume @@ -42,9 +42,8 @@ class PaintView(View): 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._force_opaque_mask = QImage(2, 2, QImage.Format.Format_Mono) - self._force_opaque_mask.fill(1) + self._paint_undo_stack.canUndoChanged.connect(self.canUndoChanged) + self._paint_undo_stack.canRedoChanged.connect(self.canRedoChanged) application = CuraApplication.getInstance() application.engineCreatedSignal.connect(self._makePaintModes) @@ -52,6 +51,15 @@ class PaintView(View): self._solid_view = None + canUndoChanged = pyqtSignal(bool) + canRedoChanged = pyqtSignal(bool) + + def canUndo(self): + return self._paint_undo_stack.canUndo() + + def canRedo(self): + return self._paint_undo_stack.canRedo() + def _makePaintModes(self): theme = CuraApplication.getInstance().getTheme() usual_types = {"none": self.PaintType(Color(*theme.getColor("paint_normal_area").getRgb()), 0), diff --git a/plugins/PaintTool/__init__.py b/plugins/PaintTool/__init__.py index e92c169ee6..637e0b00f2 100644 --- a/plugins/PaintTool/__init__.py +++ b/plugins/PaintTool/__init__.py @@ -27,7 +27,10 @@ def getMetaData(): def register(app): qmlRegisterUncreatableType(PaintTool.PaintTool.Brush, "Cura", 1, 0, "This is an enumeration class", "PaintToolBrush") + + view = PaintView.PaintView() + return { - "tool": PaintTool.PaintTool(), - "view": PaintView.PaintView() + "tool": PaintTool.PaintTool(view), + "view": view } From 8f9a17d49e7ee40a28849ccdfc41671c198dbe27 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 4 Aug 2025 16:03:09 +0200 Subject: [PATCH 151/159] Simplify QML code CURA-12661 --- plugins/PaintTool/BrushColorButton.qml | 25 ++----------------------- plugins/PaintTool/BrushShapeButton.qml | 25 ++----------------------- plugins/PaintTool/PaintModeButton.qml | 25 ++----------------------- plugins/PaintTool/PaintTool.qml | 24 ++++-------------------- 4 files changed, 10 insertions(+), 89 deletions(-) diff --git a/plugins/PaintTool/BrushColorButton.qml b/plugins/PaintTool/BrushColorButton.qml index ae4ab6243f..b62ab09e92 100644 --- a/plugins/PaintTool/BrushColorButton.qml +++ b/plugins/PaintTool/BrushColorButton.qml @@ -13,27 +13,6 @@ UM.ToolbarButton property string color - onClicked: setColor() - - function setColor() - { - UM.Controller.setProperty("BrushColor", buttonBrushColor.color); - } - - function isChecked() - { - return UM.Controller.properties.getValue("BrushColor") === buttonBrushColor.color; - } - - Component.onCompleted: - { - buttonBrushColor.checked = isChecked(); - } - - Binding - { - target: buttonBrushColor - property: "checked" - value: isChecked() - } + checked: UM.Controller.properties.getValue("BrushColor") === buttonBrushColor.color + onClicked: UM.Controller.setProperty("BrushColor", buttonBrushColor.color) } diff --git a/plugins/PaintTool/BrushShapeButton.qml b/plugins/PaintTool/BrushShapeButton.qml index ef4256792a..e05cd206f3 100644 --- a/plugins/PaintTool/BrushShapeButton.qml +++ b/plugins/PaintTool/BrushShapeButton.qml @@ -13,27 +13,6 @@ UM.ToolbarButton property int shape - onClicked: setShape() - - function setShape() - { - UM.Controller.setProperty("BrushShape", buttonBrushShape.shape) - } - - function isChecked() - { - return UM.Controller.properties.getValue("BrushShape") === buttonBrushShape.shape; - } - - Component.onCompleted: - { - buttonBrushShape.checked = isChecked(); - } - - Binding - { - target: buttonBrushShape - property: "checked" - value: isChecked() - } + checked: UM.Controller.properties.getValue("BrushShape") === buttonBrushShape.shape + onClicked: UM.Controller.setProperty("BrushShape", buttonBrushShape.shape) } diff --git a/plugins/PaintTool/PaintModeButton.qml b/plugins/PaintTool/PaintModeButton.qml index eb294f7ad6..833a009551 100644 --- a/plugins/PaintTool/PaintModeButton.qml +++ b/plugins/PaintTool/PaintModeButton.qml @@ -13,27 +13,6 @@ Cura.ModeSelectorButton property string mode - onClicked: setMode() - - function setMode() - { - UM.Controller.setProperty("PaintType", modeSelectorButton.mode); - } - - function isSelected() - { - return UM.Controller.properties.getValue("PaintType") === modeSelectorButton.mode; - } - - Component.onCompleted: - { - modeSelectorButton.selected = isSelected(); - } - - Binding - { - target: modeSelectorButton - property: "selected" - value: isSelected() - } + selected: UM.Controller.properties.getValue("PaintType") === modeSelectorButton.mode + onClicked: UM.Controller.setProperty("PaintType", modeSelectorButton.mode) } diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml index c448835bc5..01d866967a 100644 --- a/plugins/PaintTool/PaintTool.qml +++ b/plugins/PaintTool/PaintTool.qml @@ -19,6 +19,7 @@ Item { id: undoAction shortcut: "Ctrl+L" + enabled: UM.Controller.properties.getValue("CanUndo") onTriggered: UM.Controller.triggerAction("undoStackAction") } @@ -26,6 +27,7 @@ Item { id: redoAction shortcut: "Ctrl+Shift+L" + enabled: UM.Controller.properties.getValue("CanRedo") onTriggered: UM.Controller.triggerAction("redoStackAction") } @@ -53,7 +55,7 @@ Item icon: "Support" tooltipText: catalog.i18nc("@tooltip", "Refine support placement by defining preferred/avoidance areas") mode: "support" - visible: false + // visible: false } } @@ -163,6 +165,7 @@ Item from: 10 to: 1000 + value: UM.Controller.properties.getValue("BrushSize") onPressedChanged: function(pressed) { @@ -171,11 +174,6 @@ Item UM.Controller.setProperty("BrushSize", shapeSizeSlider.value); } } - - Component.onCompleted: - { - shapeSizeSlider.value = UM.Controller.properties.getValue("BrushSize"); - } } //Line between the sections. @@ -229,18 +227,4 @@ Item } } } - - Binding - { - target: undoAction - property: "enabled" - value: UM.Controller.properties.getValue("CanUndo") - } - - Binding - { - target: redoAction - property: "enabled" - value: UM.Controller.properties.getValue("CanRedo") - } } From b5e2ce6168240406169fbb1b764ff5dce030fc81 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 4 Aug 2025 16:07:49 +0200 Subject: [PATCH 152/159] Restore disabled support painting CURA-12661 --- plugins/PaintTool/PaintTool.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml index 01d866967a..0d1129eb60 100644 --- a/plugins/PaintTool/PaintTool.qml +++ b/plugins/PaintTool/PaintTool.qml @@ -55,7 +55,7 @@ Item icon: "Support" tooltipText: catalog.i18nc("@tooltip", "Refine support placement by defining preferred/avoidance areas") mode: "support" - // visible: false + visible: false } } From 9d97eb7d594208bde9891b515d59aa6673cf6362 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 5 Aug 2025 14:07:44 +0200 Subject: [PATCH 153/159] Fix sometimes wrong painting color display CURA-12660 --- plugins/PaintTool/paint.shader | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/PaintTool/paint.shader b/plugins/PaintTool/paint.shader index f2af66ffe6..c1b90b376b 100644 --- a/plugins/PaintTool/paint.shader +++ b/plugins/PaintTool/paint.shader @@ -132,7 +132,7 @@ fragment41core = [defaults] u_ambientColor = [0.3, 0.3, 0.3, 1.0] -u_opacity = 0.5 +u_opacity = 1.0 u_texture = 0 [bindings] From cf24ed91e9650dcde09f87d4d53524b878889b7d Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 5 Aug 2025 15:55:23 +0200 Subject: [PATCH 154/159] Improve fix for opacity issues CURA-12660 Previous fix caused issues when moving to preview --- plugins/PaintTool/paint.shader | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/plugins/PaintTool/paint.shader b/plugins/PaintTool/paint.shader index c1b90b376b..1982724910 100644 --- a/plugins/PaintTool/paint.shader +++ b/plugins/PaintTool/paint.shader @@ -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; @@ -58,7 +57,7 @@ fragment = 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; @@ -125,14 +123,13 @@ fragment41core = 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 = 1.0 u_texture = 0 [bindings] From e69a4369423aa57cd00d81a4b459fb6f4888c30b Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 6 Aug 2025 16:15:05 +0200 Subject: [PATCH 155/159] Fix sometimes wrongly displayed view CURA-12660 This required a refactoring of the management of the active view. The previous behavior was that anyone could set the active view, depending on certain conditions. But now we also have a view that is set by a tool, so sometimes the actually set view would be incorrect. Now each Stage requests an active view, and each tool CAN also request an active view. Then the Controller decides which view should actually be active depending on the active stage and tool. --- cura/CuraApplication.py | 5 +---- cura/Stages/CuraStage.py | 6 ++++-- plugins/PaintTool/PaintTool.py | 22 ++++++++++------------ plugins/PaintTool/PaintView.py | 22 ++++------------------ plugins/PreviewStage/PreviewStage.py | 19 ------------------- plugins/SimulationView/SimulationView.py | 16 +++++++++++++--- resources/qml/ViewsSelector.qml | 4 ++-- 7 files changed, 34 insertions(+), 60 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 5ce358080c..57d4773cb3 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1038,7 +1038,6 @@ class CuraApplication(QtApplication): # Initialize UI state controller.setActiveStage("PrepareStage") - controller.setActiveView("SolidView") controller.setCameraTool("CameraTool") controller.setSelectionTool("SelectionTool") @@ -2089,9 +2088,7 @@ class CuraApplication(QtApplication): is_non_sliceable = "." + file_extension in self._non_sliceable_extensions if is_non_sliceable: - # Need to switch first to the preview stage and then to layer view - self.callLater(lambda: (self.getController().setActiveStage("PreviewStage"), - self.getController().setActiveView("SimulationView"))) + self.callLater(lambda: (self.getController().setActiveStage("PreviewStage"))) block_slicing_decorator = BlockSlicingDecorator() node.addDecorator(block_slicing_decorator) diff --git a/cura/Stages/CuraStage.py b/cura/Stages/CuraStage.py index 869ed309dc..8c207db8ad 100644 --- a/cura/Stages/CuraStage.py +++ b/cura/Stages/CuraStage.py @@ -1,6 +1,8 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import Optional + from PyQt6.QtCore import pyqtProperty, QUrl from UM.Stage import Stage @@ -13,8 +15,8 @@ from UM.Stage import Stage # * The MainComponent is the component that will be drawn starting from the bottom of the stageBar and fills the rest # of the screen. class CuraStage(Stage): - def __init__(self, parent = None) -> None: - super().__init__(parent) + def __init__(self, parent = None, active_view: Optional[str] = "SolidView") -> None: + super().__init__(parent, active_view = active_view) @pyqtProperty(str, constant = True) def stageId(self) -> str: diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index eaeb2dc69b..7a1b25f331 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -58,7 +58,8 @@ class PaintTool(Tool): self.setExposedProperties("PaintType", "BrushSize", "BrushColor", "BrushShape") - Selection.selectionChanged.connect(self._updateIgnoreUnselectedObjects) + Selection.selectionChanged.connect(self._updateActiveView) + self._controller.activeViewChanged.connect(self._updateIgnoreUnselectedObjects) def _createBrushPen(self) -> QPen: pen = QPen() @@ -298,14 +299,9 @@ 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.setActiveView("PaintTool") # Because that's the plugin-name, and the view is registered to it. - self._updateIgnoreUnselectedObjects() return True if event.type == Event.ToolDeactivateEvent: - controller.setActiveView("SolidView") - CuraApplication.getInstance().getRenderer().getRenderPass("selection").setIgnoreUnselectedObjects(False) - CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces").setIgnoreUnselectedObjects(False) return True if event.type == Event.MouseReleaseEvent and self._controller.getToolsEnabled(): @@ -397,6 +393,9 @@ 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: @@ -404,11 +403,10 @@ class PaintTool(Tool): if node is not None: Application.getInstance().getController().getScene().sceneChanged.emit(node) - def getRequiredExtraRenderingPasses(self) -> list[str]: - return ["selection_faces", "picking_selected"] + def _updateActiveView(self): + self.setActiveView("PaintTool" if len(Selection.getAllSelectedObjects()) == 1 else None) def _updateIgnoreUnselectedObjects(self): - if self._controller.getActiveTool() is self: - ignore_unselected_objects = len(Selection.getAllSelectedObjects()) == 1 - CuraApplication.getInstance().getRenderer().getRenderPass("selection").setIgnoreUnselectedObjects(ignore_unselected_objects) - CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces").setIgnoreUnselectedObjects(ignore_unselected_objects) \ No newline at end of file + ignore_unselected_objects = self._controller.getActiveView().name == "PaintTool" + CuraApplication.getInstance().getRenderer().getRenderPass("selection").setIgnoreUnselectedObjects(ignore_unselected_objects) + CuraApplication.getInstance().getRenderer().getRenderPass("selection_faces").setIgnoreUnselectedObjects(ignore_unselected_objects) \ No newline at end of file diff --git a/plugins/PaintTool/PaintView.py b/plugins/PaintTool/PaintView.py index 73e8a511a6..22629e340c 100644 --- a/plugins/PaintTool/PaintView.py +++ b/plugins/PaintTool/PaintView.py @@ -9,8 +9,8 @@ from PyQt6.QtGui import QImage, QColor, QPainter from cura.CuraApplication import CuraApplication from cura.BuildVolume import BuildVolume +from cura.CuraView import CuraView from UM.PluginRegistry import PluginRegistry -from UM.View.View import View from UM.View.GL.ShaderProgram import ShaderProgram from UM.View.GL.Texture import Texture from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator @@ -22,7 +22,7 @@ from UM.Math.Color import Color catalog = i18nCatalog("cura") -class PaintView(View): +class PaintView(CuraView): """View for model-painting.""" UNDO_STACK_SIZE = 1024 @@ -33,7 +33,7 @@ class PaintView(View): 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) @@ -50,8 +50,6 @@ class PaintView(View): application.engineCreatedSignal.connect(self._makePaintModes) self._scene = application.getController().getScene() - self._solid_view = None - def _makePaintModes(self): theme = CuraApplication.getInstance().getTheme() usual_types = {"none": self.PaintType(Color(*theme.getColor("paint_normal_area").getRgb()), 0), @@ -179,18 +177,6 @@ class PaintView(View): if self._current_paint_type not in self._paint_modes: return - if self._solid_view is None: - plugin_registry = PluginRegistry.getInstance() - solid_view = plugin_registry.getPluginObject("SolidView") - if isinstance(solid_view, View): - self._solid_view = solid_view - - display_objects = Selection.getAllSelectedObjects().copy() - if len(display_objects) != 1 and self._solid_view is not None: - # Display the classic view until a single object is selected - self._solid_view.beginRendering() - return - self._checkSetup() renderer = self.getRenderer() @@ -201,7 +187,7 @@ class PaintView(View): paint_batch = renderer.createRenderBatch(shader=self._paint_shader) renderer.addRenderBatch(paint_batch) - for node in display_objects: + 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) diff --git a/plugins/PreviewStage/PreviewStage.py b/plugins/PreviewStage/PreviewStage.py index 88f432ef9b..3f1a4423b2 100644 --- a/plugins/PreviewStage/PreviewStage.py +++ b/plugins/PreviewStage/PreviewStage.py @@ -24,25 +24,6 @@ class PreviewStage(CuraStage): super().__init__(parent) self._application = application self._application.engineCreatedSignal.connect(self._engineCreated) - self._previously_active_view = None # type: Optional[View] - - def onStageSelected(self) -> None: - """When selecting the stage, remember which was the previous view so that - - we can revert to that view when we go out of the stage later. - """ - self._previously_active_view = self._application.getController().getActiveView() - - def onStageDeselected(self) -> None: - """Called when going to a different stage (away from the Preview Stage). - - When going to a different stage, the view should be reverted to what it - was before. Normally, that just reverts it to solid view. - """ - - if self._previously_active_view is not None: - self._application.getController().setActiveView(self._previously_active_view.getPluginId()) - self._previously_active_view = None def _engineCreated(self) -> None: """Delayed load of the QML files. diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index 083fc73bf1..5d339e7f74 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -172,13 +172,20 @@ class SimulationView(CuraView): self._updateSliceWarningVisibility() self.activityChanged.emit() - def getSimulationPass(self) -> SimulationPass: + def getSimulationPass(self) -> Optional[SimulationPass]: if not self._layer_pass: + renderer = self.getRenderer() + if renderer is None: + return None + # Currently the RenderPass constructor requires a size > 0 # This should be fixed in RenderPass's constructor. self._layer_pass = SimulationPass(1, 1) self._compatibility_mode = self._evaluateCompatibilityMode() self._layer_pass.setSimulationView(self) + self._layer_pass.setEnabled(False) + renderer.addRenderPass(self._layer_pass) + return self._layer_pass def getCurrentLayer(self) -> int: @@ -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)) diff --git a/resources/qml/ViewsSelector.qml b/resources/qml/ViewsSelector.qml index e76e5dbb67..b0e31ac532 100644 --- a/resources/qml/ViewsSelector.qml +++ b/resources/qml/ViewsSelector.qml @@ -38,7 +38,7 @@ Cura.ExpandablePopup { if (activeView == null) { - UM.Controller.setActiveView(viewModel.getItem(0).id) + UM.Controller.activeStage.setActiveView(viewModel.getItem(0).id) } } @@ -110,7 +110,7 @@ Cura.ExpandablePopup onClicked: { toggleContent() - UM.Controller.setActiveView(id) + UM.Controller.activeStage.setActiveView(id) } } } From db514f0be77dd585092ca77a1da977837b912b83 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 7 Aug 2025 11:03:51 +0200 Subject: [PATCH 156/159] Properly prepare the model for painting CURA-12660 The UV-unwrapping is now done in a background job, and the UI displays a waiting state. This fixes the issue where the user would start painting but the model was not ready yet, and the first stroke would be missing. --- plugins/PaintTool/PaintTool.py | 70 ++++++++++++++++--------- plugins/PaintTool/PaintTool.qml | 71 ++++++++++++++++++++++++++ plugins/PaintTool/PrepareTextureJob.py | 33 ++++++++++++ plugins/PaintTool/__init__.py | 1 + 4 files changed, 152 insertions(+), 23 deletions(-) create mode 100644 plugins/PaintTool/PrepareTextureJob.py diff --git a/plugins/PaintTool/PaintTool.py b/plugins/PaintTool/PaintTool.py index 7a1b25f331..82361e80ec 100644 --- a/plugins/PaintTool/PaintTool.py +++ b/plugins/PaintTool/PaintTool.py @@ -12,6 +12,7 @@ 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 @@ -22,6 +23,7 @@ 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): @@ -33,6 +35,13 @@ class PaintTool(Tool): SQUARE = 0 CIRCLE = 1 + class Paint(QObject): + @pyqtEnum + class State(IntEnum): + MULTIPLE_SELECTION = 0 # Multiple objects are selected, wait until there is only one + PREPARING_MODEL = 1 # Model is being prepared (UV-unwrapping, texture generation) + READY = 2 # Ready to paint ! + def __init__(self) -> None: super().__init__() @@ -56,10 +65,13 @@ class PaintTool(Tool): self._last_mouse_coords: Optional[Tuple[int, int]] = None self._last_face_id: Optional[int] = None - self.setExposedProperties("PaintType", "BrushSize", "BrushColor", "BrushShape") + self._state: PaintTool.Paint.State = PaintTool.Paint.State.MULTIPLE_SELECTION + self._prepare_texture_job: Optional[PrepareTextureJob] = None + + self.setExposedProperties("PaintType", "BrushSize", "BrushColor", "BrushShape", "State") - Selection.selectionChanged.connect(self._updateActiveView) self._controller.activeViewChanged.connect(self._updateIgnoreUnselectedObjects) + self._controller.activeToolChanged.connect(self._updateState) def _createBrushPen(self) -> QPen: pen = QPen() @@ -141,6 +153,9 @@ class PaintTool(Tool): self._brush_pen = self._createBrushPen() self.propertyChanged.emit() + def getState(self) -> int: + return self._state + def undoStackAction(self, redo_instead: bool) -> bool: paint_view = self._get_paint_view() if paint_view is None: @@ -266,23 +281,6 @@ class PaintTool(Tool): self._iteratateSplitSubstroke(node, substrokes, mid_struct, info_b) self._iteratateSplitSubstroke(node, substrokes, info_a, mid_struct) - def _setupNodeForPainting(self, node: SceneNode) -> bool: - mesh = node.getMeshData() - if mesh.hasUVCoordinates(): - return True - - texture_width, texture_height = mesh.calculateUnwrappedUVCoordinates() - if texture_width <= 0 or texture_height <= 0: - return False - - 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) - - return True - def event(self, event: Event) -> bool: """Handle mouse and keyboard events. @@ -304,6 +302,9 @@ class PaintTool(Tool): if event.type == Event.ToolDeactivateEvent: return True + if self._state != PaintTool.Paint.State.READY: + return False + if event.type == Event.MouseReleaseEvent and self._controller.getToolsEnabled(): if MouseEvent.LeftButton not in cast(MouseEvent, event).buttons: return False @@ -356,9 +357,6 @@ class PaintTool(Tool): if not self._mesh_transformed_cache: return False - if not self._setupNodeForPainting(node): - return False - face_id, texcoords = self._getTexCoordsFromClick(node, mouse_evt.x, mouse_evt.y) if texcoords is None: return False @@ -403,8 +401,34 @@ class PaintTool(Tool): if node is not None: Application.getInstance().getController().getScene().sceneChanged.emit(node) - def _updateActiveView(self): + 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" diff --git a/plugins/PaintTool/PaintTool.qml b/plugins/PaintTool/PaintTool.qml index 2bb3106dd5..7b04f9e58d 100644 --- a/plugins/PaintTool/PaintTool.qml +++ b/plugins/PaintTool/PaintTool.qml @@ -227,4 +227,75 @@ Item } } } + + Rectangle + { + id: waitPrepareItem + anchors.fill: parent + color: UM.Theme.getColor("main_background") + visible: UM.Controller.properties.getValue("State") === Cura.PaintToolState.PREPARING_MODEL + + ColumnLayout + { + anchors.fill: parent + + UM.Label + { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.verticalStretchFactor: 2 + + text: catalog.i18nc("@label", "Preparing model for painting...") + verticalAlignment: Text.AlignBottom + horizontalAlignment: Text.AlignHCenter + } + + Item + { + Layout.preferredWidth: loadingIndicator.width + Layout.alignment: Qt.AlignHCenter + Layout.fillHeight: true + Layout.verticalStretchFactor: 1 + + UM.ColorImage + { + id: loadingIndicator + + anchors.top: parent.top + anchors.left: parent.left + width: UM.Theme.getSize("card_icon").width + height: UM.Theme.getSize("card_icon").height + source: UM.Theme.getIcon("ArrowDoubleCircleRight") + color: UM.Theme.getColor("text_default") + + RotationAnimator + { + target: loadingIndicator + from: 0 + to: 360 + duration: 2000 + loops: Animation.Infinite + running: true + alwaysRunToEnd: true + } + } + } + } + } + + Rectangle + { + id: selectSingleMessageItem + anchors.fill: parent + color: UM.Theme.getColor("main_background") + visible: UM.Controller.properties.getValue("State") === Cura.PaintToolState.MULTIPLE_SELECTION + + UM.Label + { + anchors.fill: parent + text: catalog.i18nc("@label", "Select a single model to start painting") + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + } } diff --git a/plugins/PaintTool/PrepareTextureJob.py b/plugins/PaintTool/PrepareTextureJob.py new file mode 100644 index 0000000000..6c5e61c009 --- /dev/null +++ b/plugins/PaintTool/PrepareTextureJob.py @@ -0,0 +1,33 @@ +# Copyright (c) 2025 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from UM.Job import Job +from UM.Scene.SceneNode import SceneNode +from UM.View.GL.OpenGL import OpenGL + + +class PrepareTextureJob(Job): + """ + Background job to prepare a model for painting, i.e. do the UV-unwrapping and create the appropriate texture image, + which can last a few seconds + """ + + def __init__(self, node: SceneNode): + super().__init__() + self._node: SceneNode = node + + def run(self) -> None: + # If the model has already-provided UV coordinates, we can only assume that the associated texture + # should be a square + texture_width = texture_height = 4096 + + mesh = self._node.getMeshData() + if not mesh.hasUVCoordinates(): + texture_width, texture_height = mesh.calculateUnwrappedUVCoordinates() + + self._node.callDecoration("prepareTexture", texture_width, texture_height) + + if hasattr(mesh, OpenGL.VertexBufferProperty): + # Force clear OpenGL buffer so that new UV coordinates will be sent + delattr(mesh, OpenGL.VertexBufferProperty) + diff --git a/plugins/PaintTool/__init__.py b/plugins/PaintTool/__init__.py index e92c169ee6..93b47c7266 100644 --- a/plugins/PaintTool/__init__.py +++ b/plugins/PaintTool/__init__.py @@ -27,6 +27,7 @@ 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") return { "tool": PaintTool.PaintTool(), "view": PaintView.PaintView() From 3da366f31633a93fc42c44d845aa2fcb4ee0ef06 Mon Sep 17 00:00:00 2001 From: HellAholic <28710690+HellAholic@users.noreply.github.com> Date: Mon, 11 Aug 2025 10:58:51 +0000 Subject: [PATCH 157/159] Apply printer-linter format --- resources/definitions/fdmprinter.def.json | 2 +- resources/definitions/ultimaker.def.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index d4813429ef..68852d805f 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -9603,4 +9603,4 @@ } } } -} +} \ No newline at end of file diff --git a/resources/definitions/ultimaker.def.json b/resources/definitions/ultimaker.def.json index 2995562e59..c5f441479b 100644 --- a/resources/definitions/ultimaker.def.json +++ b/resources/definitions/ultimaker.def.json @@ -227,4 +227,4 @@ "z_seam_relative": { "value": "True" }, "zig_zaggify_support": { "value": true } } -} +} \ No newline at end of file From d372c68bd7f03b069379780cb25445aaf84f3146 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Tue, 12 Aug 2025 11:58:41 +0000 Subject: [PATCH 158/159] Set conan package version 5.10.3 --- conandata.yml | 14 +++++++------- resources/conandata.yml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/conandata.yml b/conandata.yml index 9d709096f4..da5149f15c 100644 --- a/conandata.yml +++ b/conandata.yml @@ -1,15 +1,15 @@ -version: "5.10.2" +version: "5.10.3" requirements: - - "cura_resources/5.10.2" - - "uranium/5.10.2" - - "curaengine/5.10.2" - - "cura_binary_data/5.10.2" - - "fdm_materials/5.10.2" + - "cura_resources/5.10.3" + - "uranium/5.10.3" + - "curaengine/5.10.3" + - "cura_binary_data/5.10.3" + - "fdm_materials/5.10.3" - "dulcificum/5.10.0" - "pysavitar/5.10.0" - "pynest2d/5.10.0" requirements_internal: - - "fdm_materials/5.10.2" + - "fdm_materials/5.10.3" - "cura_private_data/5.10.0-alpha.0@internal/testing" requirements_enterprise: - "native_cad_plugin/2.0.0" diff --git a/resources/conandata.yml b/resources/conandata.yml index c4418d4f57..71d63a5684 100644 --- a/resources/conandata.yml +++ b/resources/conandata.yml @@ -1 +1 @@ -version: "5.10.2" +version: "5.10.3" From a55cca73f405d1d2b5029379425068532668b320 Mon Sep 17 00:00:00 2001 From: HellAholic Date: Tue, 26 Aug 2025 13:05:56 +0200 Subject: [PATCH 159/159] Apply Review clear_mask -> clear_texture_bit_mask --- plugins/PaintTool/PaintUndoCommand.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/PaintTool/PaintUndoCommand.py b/plugins/PaintTool/PaintUndoCommand.py index c100780c7f..50bfb787b7 100644 --- a/plugins/PaintTool/PaintUndoCommand.py +++ b/plugins/PaintTool/PaintUndoCommand.py @@ -40,7 +40,7 @@ class PaintUndoCommand(QUndoCommand): bit_range_start, bit_range_end = self._bit_range full_int32 = 0xffffffff - clear_mask = full_int32 ^ (((full_int32 << (32 - 1 - (bit_range_end - bit_range_start))) & full_int32) >> ( + 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()) @@ -48,7 +48,7 @@ class PaintUndoCommand(QUndoCommand): clear_bits_image.invertPixels() painter = QPainter(clear_bits_image) painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Lighten) - painter.fillRect(image_rect, clear_mask) + painter.fillRect(image_rect, clear_texture_bit_mask) painter.end() set_value_image = self._stroke_mask.copy()