mirror of
https://github.com/Ultimaker/Cura.git
synced 2026-03-04 09:34:35 -07:00
Merge branch 'main' into CURA-12622_warn_on_actual_unused
This commit is contained in:
commit
b43bc95e2b
24 changed files with 1241 additions and 37 deletions
8
.github/workflows/find-packages.yml
vendored
8
.github/workflows/find-packages.yml
vendored
|
|
@ -1,14 +1,16 @@
|
|||
name: Find packages for Jira ticket and create installers
|
||||
name: All installers (based on Jira ticket)
|
||||
run-name: ${{ inputs.jira_ticket_number }} by @${{ github.actor }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
jira_ticket_number:
|
||||
description: 'Jira ticket number for Conan package discovery (e.g., cura_12345)'
|
||||
description: 'Jira ticket number (e.g. CURA-15432 or cura_12345)'
|
||||
required: true
|
||||
type: string
|
||||
start_builds:
|
||||
default: false
|
||||
description: 'Start installers build based on found packages'
|
||||
default: true
|
||||
required: false
|
||||
type: boolean
|
||||
conan_args:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,65 @@
|
|||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
import copy
|
||||
import json
|
||||
|
||||
from typing import Optional, Dict
|
||||
|
||||
from PyQt6.QtCore import QBuffer
|
||||
from PyQt6.QtGui import QImage, QImageWriter
|
||||
|
||||
import UM.View.GL.Texture
|
||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
from UM.View.GL.OpenGL import OpenGL
|
||||
from UM.View.GL.Texture import Texture
|
||||
|
||||
|
||||
# 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
|
||||
self._texture_data_mapping: Dict[str, tuple[int, int]] = {}
|
||||
|
||||
def isSliceable(self) -> bool:
|
||||
return True
|
||||
|
||||
def getPaintTexture(self, create_if_required: bool = True) -> Optional[UM.View.GL.Texture.Texture]:
|
||||
if self._paint_texture is None and create_if_required:
|
||||
self._paint_texture = OpenGL.getInstance().createTexture(TEXTURE_WIDTH, TEXTURE_HEIGHT)
|
||||
image = QImage(TEXTURE_WIDTH, TEXTURE_HEIGHT, QImage.Format.Format_RGB32)
|
||||
image.fill(0)
|
||||
self._paint_texture.setImage(image)
|
||||
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 packTexture(self) -> Optional[bytearray]:
|
||||
if self._paint_texture is None:
|
||||
return None
|
||||
|
||||
texture_image = self._paint_texture.getImage()
|
||||
if texture_image is None:
|
||||
return None
|
||||
|
||||
texture_buffer = QBuffer()
|
||||
texture_buffer.open(QBuffer.OpenModeFlag.ReadWrite)
|
||||
image_writer = QImageWriter(texture_buffer, b"png")
|
||||
image_writer.setText("Description", json.dumps(self._texture_data_mapping))
|
||||
image_writer.write(texture_image)
|
||||
|
||||
return texture_buffer.data()
|
||||
|
||||
def __deepcopy__(self, memo) -> "SliceableObjectDecorator":
|
||||
return type(self)()
|
||||
copied_decorator = SliceableObjectDecorator()
|
||||
copied_decorator.setPaintTexture(copy.deepcopy(self.getPaintTexture(create_if_required = False)))
|
||||
copied_decorator.setTextureDataMapping(copy.deepcopy(self.getTextureDataMapping()))
|
||||
return copied_decorator
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
# Copyright (c) 2021-2022 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import json
|
||||
import os.path
|
||||
import zipfile
|
||||
from typing import List, Optional, Union, TYPE_CHECKING, cast
|
||||
|
||||
import pySavitar as Savitar
|
||||
import numpy
|
||||
from PyQt6.QtCore import QBuffer
|
||||
from PyQt6.QtGui import QImage, QImageReader
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Math.Matrix import Matrix
|
||||
|
|
@ -18,6 +20,8 @@ from UM.Scene.GroupDecorator import GroupDecorator
|
|||
from UM.Scene.SceneNode import SceneNode # For typing.
|
||||
from UM.Scene.SceneNodeSettings import SceneNodeSettings
|
||||
from UM.Util import parseBool
|
||||
from UM.View.GL.OpenGL import OpenGL
|
||||
from UM.View.GL.Texture import Texture
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||
|
|
@ -94,14 +98,14 @@ class ThreeMFReader(MeshReader):
|
|||
return temp_mat
|
||||
|
||||
@staticmethod
|
||||
def _convertSavitarNodeToUMNode(savitar_node: Savitar.SceneNode, file_name: str = "", archive: zipfile.ZipFile = None) -> Optional[SceneNode]:
|
||||
def _convertSavitarNodeToUMNode(savitar_node: Savitar.SceneNode, file_name: str = "", archive: zipfile.ZipFile = None, scene: Savitar.Scene = None) -> Optional[SceneNode]:
|
||||
"""Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node.
|
||||
|
||||
:returns: Scene node.
|
||||
"""
|
||||
try:
|
||||
node_name = savitar_node.getName()
|
||||
node_id = savitar_node.getId()
|
||||
node_id = str(savitar_node.getId())
|
||||
except AttributeError:
|
||||
Logger.log("e", "Outdated version of libSavitar detected! Please update to the newest version!")
|
||||
node_name = ""
|
||||
|
|
@ -131,12 +135,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 +158,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)
|
||||
|
||||
|
|
@ -219,6 +230,30 @@ class ThreeMFReader(MeshReader):
|
|||
# affects (auto) slicing
|
||||
sliceable_decorator = SliceableObjectDecorator()
|
||||
um_node.addDecorator(sliceable_decorator)
|
||||
|
||||
if texture_path != "" and archive is not None:
|
||||
texture_data = archive.open(texture_path).read()
|
||||
texture_buffer = QBuffer()
|
||||
texture_buffer.open(QBuffer.OpenModeFlag.ReadWrite)
|
||||
texture_buffer.write(texture_data)
|
||||
|
||||
image_reader = QImageReader(texture_buffer, b"png")
|
||||
|
||||
texture_buffer.seek(0)
|
||||
texture_image = image_reader.read()
|
||||
texture = Texture(OpenGL.getInstance())
|
||||
texture.setImage(texture_image)
|
||||
sliceable_decorator.setPaintTexture(texture)
|
||||
|
||||
texture_buffer.seek(0)
|
||||
data_mapping_desc = image_reader.text("Description")
|
||||
if data_mapping_desc != "":
|
||||
data_mapping = json.loads(data_mapping_desc)
|
||||
for key, value in data_mapping.items():
|
||||
# Tuples are stored as lists in json, restore them back to tuples
|
||||
data_mapping[key] = tuple(value)
|
||||
sliceable_decorator.setTextureDataMapping(data_mapping)
|
||||
|
||||
return um_node
|
||||
|
||||
def _read(self, file_name: str) -> Union[SceneNode, List[SceneNode]]:
|
||||
|
|
@ -236,7 +271,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 +371,7 @@ class ThreeMFReader(MeshReader):
|
|||
# Convert the scene to scene nodes
|
||||
nodes = []
|
||||
for savitar_node in scene.getSceneNodes():
|
||||
scene_node = ThreeMFReader._convertSavitarNodeToUMNode(savitar_node, "file_name")
|
||||
scene_node = ThreeMFReader._convertSavitarNodeToUMNode(savitar_node, "file_name", scene=scene)
|
||||
if scene_node is None:
|
||||
continue
|
||||
nodes.append(scene_node)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ catalog = i18nCatalog("cura")
|
|||
|
||||
MODEL_PATH = "3D/3dmodel.model"
|
||||
PACKAGE_METADATA_PATH = "Cura/packages.json"
|
||||
TEXTURES_PATH = "3D/Textures"
|
||||
MODEL_RELATIONS_PATH = "3D/_rels/3dmodel.model.rels"
|
||||
|
||||
class ThreeMFWriter(MeshWriter):
|
||||
def __init__(self):
|
||||
|
|
@ -109,7 +111,11 @@ class ThreeMFWriter(MeshWriter):
|
|||
def _convertUMNodeToSavitarNode(um_node,
|
||||
transformation = Matrix(),
|
||||
exported_settings: Optional[Dict[str, Set[str]]] = None,
|
||||
center_mesh = False):
|
||||
center_mesh = False,
|
||||
scene: Savitar.Scene = None,
|
||||
archive: zipfile.ZipFile = None,
|
||||
model_relations_element: ET.Element = None,
|
||||
content_types_element: ET.Element = None):
|
||||
"""Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
|
||||
|
||||
:returns: Uranium Scene node.
|
||||
|
|
@ -150,7 +156,28 @@ class ThreeMFWriter(MeshWriter):
|
|||
if indices_array is not None:
|
||||
savitar_node.getMeshData().setFacesFromBytes(indices_array)
|
||||
else:
|
||||
savitar_node.getMeshData().setFacesFromBytes(numpy.arange(mesh_data.getVertices().size / 3, dtype=numpy.int32).tostring())
|
||||
savitar_node.getMeshData().setFacesFromBytes(numpy.arange(mesh_data.getVertices().size / 3, dtype=numpy.int32).tobytes())
|
||||
|
||||
packed_texture = um_node.callDecoration("packTexture")
|
||||
uv_coordinates_array = mesh_data.getUVCoordinatesAsByteArray()
|
||||
if packed_texture is not None and archive is not None and uv_coordinates_array is not None and len(uv_coordinates_array) > 0:
|
||||
texture_path = f"{TEXTURES_PATH}/{id(um_node)}.png"
|
||||
texture_file = zipfile.ZipInfo(texture_path)
|
||||
# Don't try to compress texture file, because the PNG is pretty much as compact as it will get
|
||||
archive.writestr(texture_file, packed_texture)
|
||||
|
||||
savitar_node.getMeshData().setUVCoordinatesPerVertexAsBytes(uv_coordinates_array, texture_path, scene)
|
||||
|
||||
# Add texture relation to model relations file
|
||||
if model_relations_element is not None:
|
||||
ET.SubElement(model_relations_element, "Relationship",
|
||||
Target=texture_path, Id=f"rel{len(model_relations_element)+1}",
|
||||
Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dtexture")
|
||||
|
||||
if content_types_element is not None:
|
||||
ET.SubElement(content_types_element, "Override", PartName=texture_path,
|
||||
ContentType="application/vnd.ms-package.3dmanufacturing-3dmodeltexture")
|
||||
|
||||
|
||||
# Handle per object settings (if any)
|
||||
stack = um_node.callDecoration("getStack")
|
||||
|
|
@ -187,7 +214,11 @@ class ThreeMFWriter(MeshWriter):
|
|||
if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
|
||||
continue
|
||||
savitar_child_node = ThreeMFWriter._convertUMNodeToSavitarNode(child_node,
|
||||
exported_settings = exported_settings)
|
||||
exported_settings = exported_settings,
|
||||
scene = scene,
|
||||
archive = archive,
|
||||
model_relations_element = model_relations_element,
|
||||
content_types_element = content_types_element)
|
||||
if savitar_child_node is not None:
|
||||
savitar_node.addChild(savitar_child_node)
|
||||
|
||||
|
|
@ -249,6 +280,9 @@ class ThreeMFWriter(MeshWriter):
|
|||
# Create Metadata/_rels/model_settings.config.rels
|
||||
metadata_relations_element = self._makeRelationsTree()
|
||||
|
||||
# Create model relations
|
||||
model_relations_element = self._makeRelationsTree()
|
||||
|
||||
# Let the variant add its specific files
|
||||
variant.add_extra_files(archive, metadata_relations_element)
|
||||
|
||||
|
|
@ -320,13 +354,21 @@ class ThreeMFWriter(MeshWriter):
|
|||
savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(root_child,
|
||||
transformation_matrix,
|
||||
exported_model_settings,
|
||||
center_mesh = True)
|
||||
center_mesh = True,
|
||||
scene = savitar_scene,
|
||||
archive = archive,
|
||||
model_relations_element = model_relations_element,
|
||||
content_types_element = content_types)
|
||||
if savitar_node:
|
||||
savitar_scene.addSceneNode(savitar_node)
|
||||
else:
|
||||
savitar_node = self._convertUMNodeToSavitarNode(node,
|
||||
transformation_matrix,
|
||||
exported_model_settings)
|
||||
exported_model_settings,
|
||||
scene = savitar_scene,
|
||||
archive = archive,
|
||||
model_relations_element = model_relations_element,
|
||||
content_types_element = content_types)
|
||||
if savitar_node:
|
||||
savitar_scene.addSceneNode(savitar_node)
|
||||
|
||||
|
|
@ -338,6 +380,8 @@ class ThreeMFWriter(MeshWriter):
|
|||
self._storeElementTree(archive, "_rels/.rels", relations_element)
|
||||
if len(metadata_relations_element) > 0:
|
||||
self._storeElementTree(archive, "Metadata/_rels/model_settings.config.rels", metadata_relations_element)
|
||||
if len(model_relations_element) > 0:
|
||||
self._storeElementTree(archive, MODEL_RELATIONS_PATH, model_relations_element)
|
||||
except Exception as error:
|
||||
Logger.logException("e", "Error writing zip file")
|
||||
self.setInformation(str(error))
|
||||
|
|
@ -500,7 +544,7 @@ class ThreeMFWriter(MeshWriter):
|
|||
def sceneNodesToString(scene_nodes: [SceneNode]) -> str:
|
||||
savitar_scene = Savitar.Scene()
|
||||
for scene_node in scene_nodes:
|
||||
savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(scene_node, center_mesh = True)
|
||||
savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(scene_node, center_mesh = True, scene = savitar_scene)
|
||||
savitar_scene.addSceneNode(savitar_node)
|
||||
parser = Savitar.ThreeMFParser()
|
||||
scene_string = parser.sceneToString(savitar_scene)
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ message Object
|
|||
bytes indices = 4; //An array of ints.
|
||||
repeated Setting settings = 5; // Setting override per object, overruling the global settings.
|
||||
string name = 6; //Mesh name
|
||||
bytes uv_coordinates = 7; //An array of 2 floats.
|
||||
bytes texture = 8; //PNG-encoded texture data
|
||||
}
|
||||
|
||||
message Progress
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
25
plugins/PaintTool/BrushColorButton.qml
Normal file
25
plugins/PaintTool/BrushColorButton.qml
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
25
plugins/PaintTool/BrushShapeButton.qml
Normal file
25
plugins/PaintTool/BrushShapeButton.qml
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
24
plugins/PaintTool/PaintModeButton.qml
Normal file
24
plugins/PaintTool/PaintModeButton.qml
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
347
plugins/PaintTool/PaintTool.py
Normal file
347
plugins/PaintTool/PaintTool.py
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
# Copyright (c) 2025 UltiMaker
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from enum import IntEnum
|
||||
import numpy
|
||||
from PyQt6.QtCore import Qt, QObject, pyqtEnum
|
||||
from PyQt6.QtGui import QImage, QPainter, QColor, QPen
|
||||
from PyQt6 import QtWidgets
|
||||
from typing import cast, Dict, List, Optional, Tuple
|
||||
|
||||
from numpy import ndarray
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Event import Event, MouseEvent, KeyEvent
|
||||
from UM.Logger import Logger
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.Tool import Tool
|
||||
|
||||
from cura.PickingPass import PickingPass
|
||||
from .PaintView import PaintView
|
||||
|
||||
|
||||
class PaintTool(Tool):
|
||||
"""Provides the tool to paint meshes."""
|
||||
|
||||
class Brush(QObject):
|
||||
@pyqtEnum
|
||||
class Shape(IntEnum):
|
||||
SQUARE = 0
|
||||
CIRCLE = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self._picking_pass: Optional[PickingPass] = None
|
||||
|
||||
self._shortcut_key: Qt.Key = Qt.Key.Key_P
|
||||
|
||||
self._node_cache: Optional[SceneNode] = None
|
||||
self._mesh_transformed_cache = None
|
||||
self._cache_dirty: bool = True
|
||||
|
||||
self._brush_size: int = 10
|
||||
self._brush_color: str = ""
|
||||
self._brush_shape: PaintTool.Brush.Shape = PaintTool.Brush.Shape.SQUARE
|
||||
self._brush_pen: QPen = self._createBrushPen()
|
||||
|
||||
self._mouse_held: bool = False
|
||||
|
||||
self._last_text_coords: Optional[numpy.ndarray] = None
|
||||
self._last_mouse_coords: Optional[Tuple[int, int]] = None
|
||||
self._last_face_id: Optional[int] = None
|
||||
|
||||
def _createBrushPen(self) -> QPen:
|
||||
pen = QPen()
|
||||
pen.setWidth(self._brush_size)
|
||||
pen.setColor(Qt.GlobalColor.white)
|
||||
|
||||
match self._brush_shape:
|
||||
case PaintTool.Brush.Shape.SQUARE:
|
||||
pen.setCapStyle(Qt.PenCapStyle.SquareCap)
|
||||
case PaintTool.Brush.Shape.CIRCLE:
|
||||
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
||||
return pen
|
||||
|
||||
def _createStrokeImage(self, x0: float, y0: float, x1: float, y1: float) -> Tuple[QImage, Tuple[int, int]]:
|
||||
xdiff = int(x1 - x0)
|
||||
ydiff = int(y1 - y0)
|
||||
|
||||
half_brush_size = self._brush_size // 2
|
||||
start_x = int(min(x0, x1) - half_brush_size)
|
||||
start_y = int(min(y0, y1) - half_brush_size)
|
||||
|
||||
stroke_image = QImage(abs(xdiff) + self._brush_size, abs(ydiff) + self._brush_size, QImage.Format.Format_RGB32)
|
||||
stroke_image.fill(0)
|
||||
|
||||
painter = QPainter(stroke_image)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
|
||||
painter.setPen(self._brush_pen)
|
||||
if xdiff == 0 and ydiff == 0:
|
||||
painter.drawPoint(int(x0 - start_x), int(y0 - start_y))
|
||||
else:
|
||||
painter.drawLine(int(x0 - start_x), int(y0 - start_y), int(x1 - start_x), int(y1 - start_y))
|
||||
painter.end()
|
||||
|
||||
return stroke_image, (start_x, start_y)
|
||||
|
||||
def setPaintType(self, paint_type: str) -> None:
|
||||
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:
|
||||
self._brush_size = int(brush_size)
|
||||
self._brush_pen = self._createBrushPen()
|
||||
|
||||
def setBrushColor(self, brush_color: str) -> None:
|
||||
self._brush_color = brush_color
|
||||
|
||||
def setBrushShape(self, brush_shape: int) -> None:
|
||||
if brush_shape != self._brush_shape:
|
||||
self._brush_shape = brush_shape
|
||||
self._brush_pen = self._createBrushPen()
|
||||
|
||||
def undoStackAction(self, redo_instead: bool) -> bool:
|
||||
paint_view = self._get_paint_view()
|
||||
if paint_view is None:
|
||||
return False
|
||||
|
||||
if redo_instead:
|
||||
paint_view.redoStroke()
|
||||
else:
|
||||
paint_view.undoStroke()
|
||||
|
||||
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()
|
||||
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
|
||||
if all(a == pt) or all(b == c) or all(a == c) or all(a == b):
|
||||
return 1.0
|
||||
|
||||
# compute unit vectors of directions of lines A and B
|
||||
udir_a = a - pt
|
||||
udir_a /= numpy.linalg.norm(udir_a)
|
||||
udir_b = b - c
|
||||
udir_b /= numpy.linalg.norm(udir_b)
|
||||
|
||||
# find unit direction vector for line C, which is perpendicular to lines A and B
|
||||
udir_res = numpy.cross(udir_b, udir_a)
|
||||
udir_res_len = numpy.linalg.norm(udir_res)
|
||||
if udir_res_len == 0:
|
||||
return 1.0
|
||||
udir_res /= udir_res_len
|
||||
|
||||
# solve system of equations
|
||||
rhs = b - a
|
||||
lhs = numpy.array([udir_a, -udir_b, udir_res]).T
|
||||
try:
|
||||
solved = numpy.linalg.solve(lhs, rhs)
|
||||
except numpy.linalg.LinAlgError:
|
||||
return 1.0
|
||||
|
||||
# get the ratio
|
||||
intersect = ((a + solved[0] * udir_a) + (b + solved[1] * udir_b)) * 0.5
|
||||
a_intersect_dist = numpy.linalg.norm(a - intersect)
|
||||
if a_intersect_dist == 0:
|
||||
return 1.0
|
||||
return numpy.linalg.norm(pt - intersect) / a_intersect_dist
|
||||
|
||||
def _nodeTransformChanged(self, *args) -> None:
|
||||
self._cache_dirty = True
|
||||
|
||||
def _getTexCoordsFromClick(self, node: SceneNode, x: float, y: float) -> Tuple[int, Optional[numpy.ndarray]]:
|
||||
face_id = self._selection_pass.getFaceIdAtPosition(x, y)
|
||||
if face_id < 0 or face_id >= node.getMeshData().getFaceCount():
|
||||
return face_id, None
|
||||
|
||||
pt = self._picking_pass.getPickedPosition(x, y).getData()
|
||||
|
||||
va, vb, vc = self._mesh_transformed_cache.getFaceNodes(face_id)
|
||||
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
|
||||
if wt == 0:
|
||||
return face_id, None
|
||||
wa /= wt
|
||||
wb /= wt
|
||||
wc /= wt
|
||||
texcoords = wa * ta + wb * tb + wc * tc
|
||||
return face_id, texcoords
|
||||
|
||||
def _iteratateSplitSubstroke(self, node, substrokes,
|
||||
info_a: Tuple[Tuple[float, float], Tuple[int, Optional[numpy.ndarray]]],
|
||||
info_b: Tuple[Tuple[float, float], Tuple[int, Optional[numpy.ndarray]]]) -> None:
|
||||
click_a, (face_a, texcoords_a) = info_a
|
||||
click_b, (face_b, texcoords_b) = info_b
|
||||
|
||||
if (abs(click_a[0] - click_b[0]) < 0.0001 and abs(click_a[1] - click_b[1]) < 0.0001) or (face_a < 0 and face_b < 0):
|
||||
return
|
||||
if face_b < 0 or face_a == face_b:
|
||||
substrokes.append((self._last_text_coords, texcoords_a))
|
||||
return
|
||||
if face_a < 0:
|
||||
substrokes.append((self._last_text_coords, texcoords_b))
|
||||
return
|
||||
|
||||
mouse_mid = (click_a[0] + click_b[0]) / 2.0, (click_a[1] + click_b[1]) / 2.0
|
||||
face_mid, texcoords_mid = self._getTexCoordsFromClick(node, mouse_mid[0], mouse_mid[1])
|
||||
mid_struct = (mouse_mid, (face_mid, texcoords_mid))
|
||||
if face_mid == face_a:
|
||||
substrokes.append((texcoords_a, texcoords_mid))
|
||||
self._iteratateSplitSubstroke(node, substrokes, mid_struct, info_b)
|
||||
elif face_mid == face_b:
|
||||
substrokes.append((texcoords_mid, texcoords_b))
|
||||
self._iteratateSplitSubstroke(node, substrokes, info_a, mid_struct)
|
||||
else:
|
||||
self._iteratateSplitSubstroke(node, substrokes, mid_struct, info_b)
|
||||
self._iteratateSplitSubstroke(node, substrokes, info_a, mid_struct)
|
||||
|
||||
def event(self, event: Event) -> bool:
|
||||
"""Handle mouse and keyboard events.
|
||||
|
||||
:param event: The event to handle.
|
||||
:return: Whether this event has been caught by this tool (True) or should
|
||||
be passed on (False).
|
||||
"""
|
||||
super().event(event)
|
||||
|
||||
controller = Application.getInstance().getController()
|
||||
node = Selection.getSelectedObject(0)
|
||||
if node is None:
|
||||
return False
|
||||
|
||||
# Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes
|
||||
if event.type == Event.ToolActivateEvent:
|
||||
controller.setActiveStage("PrepareStage")
|
||||
controller.setActiveView("PaintTool") # Because that's the plugin-name, and the view is registered to it.
|
||||
return True
|
||||
|
||||
if event.type == Event.ToolDeactivateEvent:
|
||||
controller.setActiveStage("PrepareStage")
|
||||
controller.setActiveView("SolidView")
|
||||
return True
|
||||
|
||||
if event.type == Event.MouseReleaseEvent and self._controller.getToolsEnabled():
|
||||
if MouseEvent.LeftButton not in cast(MouseEvent, event).buttons:
|
||||
return False
|
||||
self._mouse_held = False
|
||||
self._last_text_coords = None
|
||||
self._last_mouse_coords = None
|
||||
self._last_face_id = None
|
||||
return True
|
||||
|
||||
is_moved = event.type == Event.MouseMoveEvent
|
||||
is_pressed = event.type == Event.MousePressEvent
|
||||
if (is_moved or is_pressed) and self._controller.getToolsEnabled():
|
||||
if is_moved and not self._mouse_held:
|
||||
return False
|
||||
|
||||
mouse_evt = cast(MouseEvent, event)
|
||||
if is_pressed:
|
||||
if MouseEvent.LeftButton not in mouse_evt.buttons:
|
||||
return False
|
||||
else:
|
||||
self._mouse_held = True
|
||||
|
||||
paintview = self._get_paint_view()
|
||||
if paintview is None:
|
||||
return False
|
||||
|
||||
if not self._selection_pass:
|
||||
return False
|
||||
|
||||
camera = self._controller.getScene().getActiveCamera()
|
||||
if not camera:
|
||||
return False
|
||||
|
||||
if node != self._node_cache:
|
||||
if self._node_cache is not None:
|
||||
self._node_cache.transformationChanged.disconnect(self._nodeTransformChanged)
|
||||
self._node_cache = node
|
||||
self._node_cache.transformationChanged.connect(self._nodeTransformChanged)
|
||||
if self._cache_dirty:
|
||||
self._cache_dirty = False
|
||||
self._mesh_transformed_cache = self._node_cache.getMeshDataTransformed()
|
||||
if not self._mesh_transformed_cache:
|
||||
return False
|
||||
|
||||
if not self._picking_pass:
|
||||
self._picking_pass = PickingPass(camera.getViewportWidth(), camera.getViewportHeight())
|
||||
self._picking_pass.render()
|
||||
|
||||
self._selection_pass.renderFacesMode()
|
||||
|
||||
face_id, texcoords = self._getTexCoordsFromClick(node, mouse_evt.x, mouse_evt.y)
|
||||
if texcoords is None:
|
||||
return False
|
||||
if self._last_text_coords is None:
|
||||
self._last_text_coords = texcoords
|
||||
self._last_mouse_coords = (mouse_evt.x, mouse_evt.y)
|
||||
self._last_face_id = face_id
|
||||
|
||||
substrokes = []
|
||||
if face_id == self._last_face_id:
|
||||
substrokes.append((self._last_text_coords, texcoords))
|
||||
else:
|
||||
self._iteratateSplitSubstroke(node, substrokes,
|
||||
(self._last_mouse_coords, (self._last_face_id, self._last_text_coords)),
|
||||
((mouse_evt.x, mouse_evt.y), (face_id, texcoords)))
|
||||
|
||||
w, h = paintview.getUvTexDimensions()
|
||||
for start_coords, end_coords in substrokes:
|
||||
sub_image, (start_x, start_y) = self._createStrokeImage(
|
||||
start_coords[0] * w,
|
||||
start_coords[1] * h,
|
||||
end_coords[0] * w,
|
||||
end_coords[1] * h
|
||||
)
|
||||
paintview.addStroke(sub_image, start_x, start_y, self._brush_color)
|
||||
|
||||
self._last_text_coords = texcoords
|
||||
self._last_mouse_coords = (mouse_evt.x, mouse_evt.y)
|
||||
self._last_face_id = face_id
|
||||
self._updateScene(node)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@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)
|
||||
237
plugins/PaintTool/PaintTool.qml
Normal file
237
plugins/PaintTool/PaintTool.qml
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
// Copyright (c) 2025 UltiMaker
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import UM 1.7 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
||||
Item
|
||||
{
|
||||
id: base
|
||||
width: childrenRect.width
|
||||
height: childrenRect.height
|
||||
UM.I18nCatalog { id: catalog; name: "cura"}
|
||||
|
||||
property string selectedMode: ""
|
||||
property string selectedColor: ""
|
||||
property int selectedShape: 0
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
Column
|
||||
{
|
||||
id: mainColumn
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
|
||||
RowLayout
|
||||
{
|
||||
id: rowPaintMode
|
||||
width: parent.width
|
||||
|
||||
PaintModeButton
|
||||
{
|
||||
text: catalog.i18nc("@action:button", "Seam")
|
||||
icon: "Seam"
|
||||
tooltipText: catalog.i18nc("@tooltip", "Refine seam placement by defining preferred/avoidance areas")
|
||||
mode: "seam"
|
||||
}
|
||||
|
||||
PaintModeButton
|
||||
{
|
||||
text: catalog.i18nc("@action:button", "Support")
|
||||
icon: "Support"
|
||||
tooltipText: catalog.i18nc("@tooltip", "Refine support placement by defining preferred/avoidance areas")
|
||||
mode: "support"
|
||||
}
|
||||
}
|
||||
|
||||
//Line between the sections.
|
||||
Rectangle
|
||||
{
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("default_lining").height
|
||||
color: UM.Theme.getColor("lining")
|
||||
}
|
||||
|
||||
RowLayout
|
||||
{
|
||||
id: rowBrushColor
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@label", "Mark as")
|
||||
}
|
||||
|
||||
BrushColorButton
|
||||
{
|
||||
id: buttonPreferredArea
|
||||
color: "preferred"
|
||||
|
||||
text: catalog.i18nc("@action:button", "Preferred")
|
||||
toolItem: UM.ColorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("CheckBadge", "low")
|
||||
color: UM.Theme.getColor("paint_preferred_area")
|
||||
}
|
||||
}
|
||||
|
||||
BrushColorButton
|
||||
{
|
||||
id: buttonAvoidArea
|
||||
color: "avoid"
|
||||
|
||||
text: catalog.i18nc("@action:button", "Avoid")
|
||||
toolItem: UM.ColorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("CancelBadge", "low")
|
||||
color: UM.Theme.getColor("paint_avoid_area")
|
||||
}
|
||||
}
|
||||
|
||||
BrushColorButton
|
||||
{
|
||||
id: buttonEraseArea
|
||||
color: "none"
|
||||
|
||||
text: catalog.i18nc("@action:button", "Erase")
|
||||
toolItem: UM.ColorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("Eraser")
|
||||
color: UM.Theme.getColor("icon")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout
|
||||
{
|
||||
id: rowBrushShape
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@label", "Brush Shape")
|
||||
}
|
||||
|
||||
BrushShapeButton
|
||||
{
|
||||
id: buttonBrushCircle
|
||||
shape: Cura.PaintToolBrush.CIRCLE
|
||||
|
||||
text: catalog.i18nc("@action:button", "Circle")
|
||||
toolItem: UM.ColorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("Circle")
|
||||
color: UM.Theme.getColor("icon")
|
||||
}
|
||||
}
|
||||
|
||||
BrushShapeButton
|
||||
{
|
||||
id: buttonBrushSquare
|
||||
shape: Cura.PaintToolBrush.SQUARE
|
||||
|
||||
text: catalog.i18nc("@action:button", "Square")
|
||||
toolItem: UM.ColorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("MeshTypeNormal")
|
||||
color: UM.Theme.getColor("icon")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@label", "Brush Size")
|
||||
}
|
||||
|
||||
UM.Slider
|
||||
{
|
||||
id: shapeSizeSlider
|
||||
width: parent.width
|
||||
indicatorVisible: false
|
||||
|
||||
from: 1
|
||||
to: 40
|
||||
value: 10
|
||||
|
||||
onPressedChanged: function(pressed)
|
||||
{
|
||||
if(! pressed)
|
||||
{
|
||||
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
|
||||
{
|
||||
id: undoButton
|
||||
|
||||
text: catalog.i18nc("@action:button", "Undo Stroke")
|
||||
toolItem: UM.ColorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("ArrowReset")
|
||||
color: UM.Theme.getColor("icon")
|
||||
}
|
||||
|
||||
onClicked: undoAction.trigger()
|
||||
}
|
||||
|
||||
UM.ToolbarButton
|
||||
{
|
||||
id: redoButton
|
||||
|
||||
text: catalog.i18nc("@action:button", "Redo Stroke")
|
||||
toolItem: UM.ColorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("ArrowReset")
|
||||
color: UM.Theme.getColor("icon")
|
||||
transform: [
|
||||
Scale { xScale: -1; origin.x: width/2 }
|
||||
]
|
||||
}
|
||||
|
||||
onClicked: redoAction.trigger()
|
||||
}
|
||||
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
id: clearButton
|
||||
text: catalog.i18nc("@button", "Clear all")
|
||||
onClicked: UM.Controller.triggerAction("clear")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
186
plugins/PaintTool/PaintView.py
Normal file
186
plugins/PaintTool/PaintView.py
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
# Copyright (c) 2025 UltiMaker
|
||||
# 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
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
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
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Math.Color import Color
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class PaintView(View):
|
||||
"""View for model-painting."""
|
||||
|
||||
UNDO_STACK_SIZE = 1024
|
||||
|
||||
class PaintType:
|
||||
def __init__(self, display_color: Color, value: int):
|
||||
self.display_color: Color = display_color
|
||||
self.value: int = value
|
||||
|
||||
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, Dict[str, "PaintView.PaintType"]] = {}
|
||||
|
||||
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)
|
||||
|
||||
CuraApplication.getInstance().engineCreatedSignal.connect(self._makePaintModes)
|
||||
|
||||
def _makePaintModes(self):
|
||||
theme = CuraApplication.getInstance().getTheme()
|
||||
usual_types = {"none": self.PaintType(Color(*theme.getColor("paint_normal_area").getRgb()), 0),
|
||||
"preferred": self.PaintType(Color(*theme.getColor("paint_preferred_area").getRgb()), 1),
|
||||
"avoid": self.PaintType(Color(*theme.getColor("paint_avoid_area").getRgb()), 2)}
|
||||
self._paint_modes = {
|
||||
"seam": usual_types,
|
||||
"support": usual_types,
|
||||
}
|
||||
|
||||
def _checkSetup(self):
|
||||
if not self._paint_shader:
|
||||
shader_filename = os.path.join(PluginRegistry.getInstance().getPluginPath("PaintTool"), "paint.shader")
|
||||
self._paint_shader = OpenGL.getInstance().createShaderProgram(shader_filename)
|
||||
|
||||
def _forceOpaqueDeepCopy(self, image: QImage):
|
||||
res = QImage(image.width(), image.height(), QImage.Format.Format_RGBA8888)
|
||||
res.fill(QColor(255, 255, 255, 255))
|
||||
painter = QPainter(res)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
|
||||
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
|
||||
painter.drawImage(0, 0, image)
|
||||
painter.end()
|
||||
res.setAlphaChannel(self._force_opaque_mask.scaled(image.width(), image.height()))
|
||||
return res
|
||||
|
||||
def addStroke(self, stroke_mask: QImage, start_x: int, start_y: int, brush_color: str) -> None:
|
||||
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][brush_color].value << self._current_bits_ranges[0]
|
||||
full_int32 = 0xffffffff
|
||||
clear_mask = full_int32 ^ (((full_int32 << (32 - 1 - (bit_range_end - bit_range_start))) & full_int32) >> (32 - 1 - bit_range_end))
|
||||
image_rect = QRect(0, 0, stroke_mask.width(), stroke_mask.height())
|
||||
|
||||
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_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_mask.width(), stroke_mask.height())
|
||||
painter = QPainter(stroked_image)
|
||||
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceAndDestination)
|
||||
painter.drawImage(0, 0, clear_bits_image)
|
||||
painter.setCompositionMode(QPainter.CompositionMode.RasterOp_SourceOrDestination)
|
||||
painter.drawImage(0, 0, set_value_image)
|
||||
painter.end()
|
||||
|
||||
self._stroke_redo_stack.clear()
|
||||
if len(self._stroke_undo_stack) >= PaintView.UNDO_STACK_SIZE:
|
||||
self._stroke_undo_stack.pop(0)
|
||||
undo_image = self._forceOpaqueDeepCopy(self._current_paint_texture.setSubImage(stroked_image, start_x, start_y))
|
||||
if undo_image is not None:
|
||||
self._stroke_undo_stack.append((undo_image, start_x, start_y))
|
||||
|
||||
def _applyUndoStacksAction(self, from_stack: List[Tuple[QImage, int, int]], to_stack: List[Tuple[QImage, int, int]]) -> bool:
|
||||
if len(from_stack) <= 0 or self._current_paint_texture is None:
|
||||
return False
|
||||
from_image, x, y = from_stack.pop()
|
||||
to_image = self._forceOpaqueDeepCopy(self._current_paint_texture.setSubImage(from_image, x, y))
|
||||
if to_image is None:
|
||||
return False
|
||||
if len(to_stack) >= PaintView.UNDO_STACK_SIZE:
|
||||
to_stack.pop(0)
|
||||
to_stack.append((to_image, x, y))
|
||||
return True
|
||||
|
||||
def undoStroke(self) -> bool:
|
||||
return self._applyUndoStacksAction(self._stroke_undo_stack, self._stroke_redo_stack)
|
||||
|
||||
def redoStroke(self) -> bool:
|
||||
return self._applyUndoStacksAction(self._stroke_redo_stack, self._stroke_undo_stack)
|
||||
|
||||
def getUvTexDimensions(self):
|
||||
if self._current_paint_texture is not None:
|
||||
return self._current_paint_texture.getWidth(), self._current_paint_texture.getHeight()
|
||||
return 0, 0
|
||||
|
||||
def 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]))
|
||||
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()
|
||||
paint_batch = renderer.createRenderBatch(shader=self._paint_shader)
|
||||
renderer.addRenderBatch(paint_batch)
|
||||
|
||||
node = Selection.getSelectedObject(0)
|
||||
if node is None:
|
||||
return
|
||||
|
||||
if self._current_paint_type == "":
|
||||
return
|
||||
|
||||
self._paint_shader.setUniformValue("u_bitsRangesStart", self._current_bits_ranges[0])
|
||||
self._paint_shader.setUniformValue("u_bitsRangesEnd", self._current_bits_ranges[1])
|
||||
|
||||
colors = [paint_type_obj.display_color for paint_type_obj in self._paint_modes[self._current_paint_type].values()]
|
||||
colors_values = [[int(color_part * 255) for color_part in [color.r, color.g, color.b]] for color in colors]
|
||||
self._paint_shader.setUniformValueArray("u_renderColors", colors_values)
|
||||
|
||||
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())
|
||||
33
plugins/PaintTool/__init__.py
Normal file
33
plugins/PaintTool/__init__.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Copyright (c) 2025 UltiMaker
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from . import PaintTool
|
||||
from . import PaintView
|
||||
|
||||
from PyQt6.QtQml import qmlRegisterUncreatableType
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"tool": {
|
||||
"name": i18n_catalog.i18nc("@action:button", "Paint"),
|
||||
"description": i18n_catalog.i18nc("@info:tooltip", "Paint Model"),
|
||||
"icon": "Visual",
|
||||
"tool_panel": "PaintTool.qml",
|
||||
"weight": 0
|
||||
},
|
||||
"view": {
|
||||
"name": i18n_catalog.i18nc("@item:inmenu", "Paint view"),
|
||||
"weight": 0,
|
||||
"visible": False
|
||||
}
|
||||
}
|
||||
|
||||
def register(app):
|
||||
qmlRegisterUncreatableType(PaintTool.PaintTool.Brush, "Cura", 1, 0, "This is an enumeration class", "PaintToolBrush")
|
||||
return {
|
||||
"tool": PaintTool.PaintTool(),
|
||||
"view": PaintView.PaintView()
|
||||
}
|
||||
149
plugins/PaintTool/paint.shader
Normal file
149
plugins/PaintTool/paint.shader
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
[shaders]
|
||||
vertex =
|
||||
uniform highp mat4 u_modelMatrix;
|
||||
uniform highp mat4 u_viewMatrix;
|
||||
uniform highp mat4 u_projectionMatrix;
|
||||
|
||||
uniform highp mat4 u_normalMatrix;
|
||||
|
||||
attribute highp vec4 a_vertex;
|
||||
attribute highp vec4 a_normal;
|
||||
attribute highp vec2 a_uvs;
|
||||
|
||||
varying highp vec3 v_vertex;
|
||||
varying highp vec3 v_normal;
|
||||
varying highp vec2 v_uvs;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec4 world_space_vert = u_modelMatrix * a_vertex;
|
||||
gl_Position = u_projectionMatrix * u_viewMatrix * world_space_vert;
|
||||
|
||||
v_vertex = world_space_vert.xyz;
|
||||
v_normal = (u_normalMatrix * normalize(a_normal)).xyz;
|
||||
|
||||
v_uvs = a_uvs;
|
||||
}
|
||||
|
||||
fragment =
|
||||
uniform mediump vec4 u_ambientColor;
|
||||
uniform highp vec3 u_lightPosition;
|
||||
uniform highp vec3 u_viewPosition;
|
||||
uniform 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;
|
||||
varying highp vec2 v_uvs;
|
||||
|
||||
void main()
|
||||
{
|
||||
mediump vec4 final_color = vec4(0.0);
|
||||
|
||||
/* Ambient Component */
|
||||
final_color += u_ambientColor;
|
||||
|
||||
highp vec3 normal = normalize(v_normal);
|
||||
highp vec3 light_dir = normalize(u_lightPosition - v_vertex);
|
||||
|
||||
/* Diffuse Component */
|
||||
ivec4 texture = ivec4(texture(u_texture, v_uvs) * 255.0);
|
||||
uint color_index = (texture.r << 16) | (texture.g << 8) | texture.b;
|
||||
color_index = (color_index << (32 - 1 - u_bitsRangesEnd)) >> 32 - 1 - (u_bitsRangesEnd - u_bitsRangesStart);
|
||||
|
||||
vec4 diffuse_color = vec4(u_renderColors[color_index] / 255.0, 1.0);
|
||||
highp float n_dot_l = clamp(dot(normal, light_dir), 0.0, 1.0);
|
||||
final_color += (n_dot_l * diffuse_color);
|
||||
|
||||
final_color.a = u_opacity;
|
||||
|
||||
frag_color = final_color;
|
||||
}
|
||||
|
||||
vertex41core =
|
||||
#version 410
|
||||
uniform highp mat4 u_modelMatrix;
|
||||
uniform highp mat4 u_viewMatrix;
|
||||
uniform highp mat4 u_projectionMatrix;
|
||||
|
||||
uniform highp mat4 u_normalMatrix;
|
||||
|
||||
in highp vec4 a_vertex;
|
||||
in highp vec4 a_normal;
|
||||
in highp vec2 a_uvs;
|
||||
|
||||
out highp vec3 v_vertex;
|
||||
out highp vec3 v_normal;
|
||||
out highp vec2 v_uvs;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec4 world_space_vert = u_modelMatrix * a_vertex;
|
||||
gl_Position = u_projectionMatrix * u_viewMatrix * world_space_vert;
|
||||
|
||||
v_vertex = world_space_vert.xyz;
|
||||
v_normal = (u_normalMatrix * normalize(a_normal)).xyz;
|
||||
|
||||
v_uvs = a_uvs;
|
||||
}
|
||||
|
||||
fragment41core =
|
||||
#version 410
|
||||
uniform mediump vec4 u_ambientColor;
|
||||
uniform highp vec3 u_lightPosition;
|
||||
uniform highp vec3 u_viewPosition;
|
||||
uniform 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;
|
||||
in highp vec2 v_uvs;
|
||||
out vec4 frag_color;
|
||||
|
||||
void main()
|
||||
{
|
||||
mediump vec4 final_color = vec4(0.0);
|
||||
|
||||
/* Ambient Component */
|
||||
final_color += u_ambientColor;
|
||||
|
||||
highp vec3 normal = normalize(v_normal);
|
||||
highp vec3 light_dir = normalize(u_lightPosition - v_vertex);
|
||||
|
||||
/* Diffuse Component */
|
||||
ivec4 texture = ivec4(texture(u_texture, v_uvs) * 255.0);
|
||||
uint color_index = (texture.r << 16) | (texture.g << 8) | texture.b;
|
||||
color_index = (color_index << (32 - 1 - u_bitsRangesEnd)) >> 32 - 1 - (u_bitsRangesEnd - u_bitsRangesStart);
|
||||
|
||||
vec4 diffuse_color = vec4(u_renderColors[color_index] / 255.0, 1.0);
|
||||
highp float n_dot_l = clamp(dot(normal, light_dir), 0.0, 1.0);
|
||||
final_color += (n_dot_l * diffuse_color);
|
||||
|
||||
final_color.a = u_opacity;
|
||||
|
||||
frag_color = final_color;
|
||||
}
|
||||
|
||||
[defaults]
|
||||
u_ambientColor = [0.3, 0.3, 0.3, 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
|
||||
8
plugins/PaintTool/plugin.json
Normal file
8
plugins/PaintTool/plugin.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "Paint Tools",
|
||||
"author": "UltiMaker",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides the paint tools.",
|
||||
"api": 8,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os.path
|
||||
from UM.View.View import View
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.Resources import Resources
|
||||
from PyQt6.QtGui import QOpenGLContext, QDesktopServices, QImage
|
||||
from PyQt6.QtCore import QSize, QUrl
|
||||
from PyQt6.QtGui import QDesktopServices, QImage
|
||||
from PyQt6.QtCore import QUrl
|
||||
|
||||
import numpy as np
|
||||
import time
|
||||
|
|
@ -36,11 +35,12 @@ class SolidView(View):
|
|||
"""Standard view for mesh models."""
|
||||
|
||||
_show_xray_warning_preference = "view/show_xray_warning"
|
||||
_show_overhang_preference = "view/show_overhang"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
application = Application.getInstance()
|
||||
application.getPreferences().addPreference("view/show_overhang", True)
|
||||
application.getPreferences().addPreference(self._show_overhang_preference, True)
|
||||
application.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||
self._enabled_shader = None
|
||||
self._disabled_shader = None
|
||||
|
|
@ -212,7 +212,7 @@ class SolidView(View):
|
|||
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
if Application.getInstance().getPreferences().getValue("view/show_overhang"):
|
||||
if Application.getInstance().getPreferences().getValue(self._show_overhang_preference):
|
||||
# Make sure the overhang angle is valid before passing it to the shader
|
||||
if self._support_angle >= 0 and self._support_angle <= 90:
|
||||
self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(90 - self._support_angle)))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 : ""
|
||||
|
|
|
|||
5
resources/themes/cura-light/icons/default/Circle.svg
Normal file
5
resources/themes/cura-light/icons/default/Circle.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M 12,2 C 6.5,2 2,6.5 2,12 2,17.5 6.5,22 12,22 17.5,22 22,17.5 22,12 22,6.5 17.5,2 12,2 Z m 0,18 C 7.6,20 4,16.4 4,12 4,7.6 7.6,4 12,4 c 4.4,0 8,3.6 8,8 0,4.4 -3.6,8 -8,8 z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 313 B |
5
resources/themes/cura-light/icons/default/Eraser.svg
Normal file
5
resources/themes/cura-light/icons/default/Eraser.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="22" height="21" viewBox="0 0 22 21" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.5 19.25H4.24999V20.75H21.5V19.25Z" />
|
||||
<path d="M19.535 6.88249L13.5875 0.942493C13.4482 0.803029 13.2827 0.69239 13.1006 0.616904C12.9185 0.541417 12.7234 0.502563 12.5262 0.502563C12.3291 0.502563 12.1339 0.541417 11.9518 0.616904C11.7697 0.69239 11.6043 0.803029 11.465 0.942493L0.964985 11.4425C0.82552 11.5818 0.714882 11.7472 0.639395 11.9293C0.563909 12.1114 0.525055 12.3066 0.525055 12.5037C0.525055 12.7009 0.563909 12.8961 0.639395 13.0782C0.714882 13.2603 0.82552 13.4257 0.964985 13.565L4.34749 17H11.54L19.535 9.00499C19.6745 8.86568 19.7851 8.70025 19.8606 8.51815C19.9361 8.33606 19.9749 8.14087 19.9749 7.94374C19.9749 7.74662 19.9361 7.55143 19.8606 7.36933C19.7851 7.18724 19.6745 7.0218 19.535 6.88249ZM10.9175 15.5H4.99999L1.99998 12.5L6.73249 7.76749L12.68 13.7075L10.9175 15.5ZM13.7375 12.68L7.79749 6.73249L12.5 1.99999L18.5 7.94749L13.7375 12.68Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1,018 B |
6
resources/themes/cura-light/icons/default/Seam.svg
Normal file
6
resources/themes/cura-light/icons/default/Seam.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
id="path2"
|
||||
d="M 6 3 C 4.3000017 3 3 4.3000017 3 6 L 3 18 C 3 19.699998 4.3000017 21 6 21 L 18 21 C 19.699998 21 21 19.699998 21 18 L 21 6 C 21 4.3000017 19.699998 3 18 3 L 6 3 z M 5 5 L 7.6972656 5 L 7.6972656 19 L 5 19 L 5 5 z M 9.5957031 5 L 19 5 L 19 19 L 9.5957031 19 L 9.5957031 16.517578 L 11.369141 16.517578 L 11.369141 14.617188 L 9.5957031 14.617188 L 9.5957031 12.958984 L 11.802734 12.958984 L 11.802734 11.058594 L 9.5957031 11.058594 L 9.5957031 8.890625 L 11.369141 8.890625 L 11.369141 6.9902344 L 9.5957031 6.9902344 L 9.5957031 5 z " />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 694 B |
5
resources/themes/cura-light/icons/low/CancelBadge.svg
Normal file
5
resources/themes/cura-light/icons/low/CancelBadge.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||
<path
|
||||
d="M 6 1 C 3.2385791 1 1 3.2385791 1 6 C 1 8.7614209 3.2385791 11 6 11 C 8.7614209 11 11 8.7614209 11 6 C 11 3.2385791 8.7614209 1 6 1 z M 3.9179688 3.0332031 L 6 5.1171875 L 8.0820312 3.0332031 L 8.9667969 3.9179688 L 6.8828125 6 L 8.9667969 8.0820312 L 8.0820312 8.9667969 L 6 6.8828125 L 3.9179688 8.9667969 L 3.0332031 8.0820312 L 5.1171875 6 L 3.0332031 3.9179688 L 3.9179688 3.0332031 z " />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 532 B |
5
resources/themes/cura-light/icons/low/CheckBadge.svg
Normal file
5
resources/themes/cura-light/icons/low/CheckBadge.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||
<path
|
||||
d="M 6 1 A 5 5 0 0 0 1 6 A 5 5 0 0 0 6 11 A 5 5 0 0 0 11 6 A 5 5 0 0 0 6 1 z M 8.4921875 3.6542969 L 9.3027344 4.4648438 L 5.4199219 8.3457031 L 2.6972656 5.6230469 L 3.5078125 4.8125 L 5.4199219 6.7246094 L 8.4921875 3.6542969 z " />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 369 B |
|
|
@ -463,8 +463,6 @@
|
|||
"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],
|
||||
|
|
@ -504,7 +502,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": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue