From 93694e2da4090103ce121621efd0fa0a6b978882 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 20 May 2025 15:51:13 +0200 Subject: [PATCH 01/37] 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 02/37] 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 03/37] 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 04/37] 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 05/37] 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 06/37] 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 704f9453f0a1cfd554eb4b18cb31b293fa4c917e Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 27 May 2025 17:03:38 +0200 Subject: [PATCH 07/37] 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 08/37] 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 09/37] 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 10/37] 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 11/37] 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 12/37] 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 13/37] 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 14/37] 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 15/37] 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 16/37] 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 12d788db62facc68334820216b9a38f70806cb80 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 4 Jun 2025 21:00:27 +0200 Subject: [PATCH 17/37] 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 18/37] 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 19/37] 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 b358b93b690bd2106c9c68f50da5f3c73b11fafe Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 10 Jun 2025 08:30:43 +0200 Subject: [PATCH 20/37] 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 2390067d694b425eb87a3830ce3647258f07fc1c Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 17 Jun 2025 09:20:59 +0200 Subject: [PATCH 21/37] 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 22/37] 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 75946d8871b43a1e091ac36900d8c2cbfdd0ffd3 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 18 Jun 2025 11:56:08 +0200 Subject: [PATCH 23/37] 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 24/37] 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 25/37] 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 26/37] 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 d2ade67cad0a670ea82fd0cd4ada9fdc97c7f18b Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 18 Jun 2025 13:41:20 +0200 Subject: [PATCH 27/37] 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 28/37] 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 29/37] 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 960d2a231592930cf880cebff66fbd98dfef99b9 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 19 Jun 2025 10:15:37 +0200 Subject: [PATCH 30/37] 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 31/37] 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 7dcc5cd4701d66abc5de9d5e4c8815be11802fbf Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 24 Jun 2025 13:11:00 +0200 Subject: [PATCH 32/37] 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 33/37] 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 bbddcab4e9520af04e390dc7c9ffe739488ddef1 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 2 Jul 2025 14:04:41 +0200 Subject: [PATCH 34/37] 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 4aea5807cfe17dc8c0910be9d6e265e21b4d7644 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 4 Jul 2025 17:25:49 +0200 Subject: [PATCH 35/37] 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 b1676c66fdc7ceea2a82e964219c1059fdced9bf Mon Sep 17 00:00:00 2001 From: HellAholic Date: Thu, 17 Jul 2025 09:15:00 +0200 Subject: [PATCH 36/37] 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 c7b86ae1c43480c946fe4bd2885ec0e2db5f2d7f Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 21 Jul 2025 12:00:05 +0200 Subject: [PATCH 37/37] 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: