mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-08-18 07:36:04 +08:00
Merge remote-tracking branch 'origin/CURA-12543_painting_ux' into CURA-12544_saving-and-loading-painted-files-in-Cura
This commit is contained in:
commit
5873222c15
246
plugins/PaintTool/PaintTool.py
Normal file
246
plugins/PaintTool/PaintTool.py
Normal file
@ -0,0 +1,246 @@
|
||||
# Copyright (c) 2025 UltiMaker
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import numpy
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtGui import QImage, QPainter, QColor, QBrush, QPen
|
||||
from typing import cast, Dict, List, Optional, 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 cura.PickingPass import PickingPass
|
||||
from .PaintView import PaintView
|
||||
|
||||
|
||||
class PaintTool(Tool):
|
||||
"""Provides the tool to paint meshes."""
|
||||
|
||||
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._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"
|
||||
self._brush_pen: Optional[QPen] = None
|
||||
|
||||
self._mouse_held: bool = False
|
||||
self._last_text_coords: Optional[Tuple[int, int]] = None
|
||||
|
||||
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]))
|
||||
match self._brush_shape:
|
||||
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]]:
|
||||
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_RGBA8888)
|
||||
stroke_image.fill(QColor(0,0,0,0))
|
||||
|
||||
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))
|
||||
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._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_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()
|
||||
|
||||
def setBrushShape(self, brush_shape: str) -> None:
|
||||
if brush_shape != self._brush_shape:
|
||||
self._brush_shape = brush_shape
|
||||
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:
|
||||
# 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 _nodeTransformChanged(self, *args) -> None:
|
||||
self._cache_dirty = True
|
||||
|
||||
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 or face_id >= node.getMeshData().getFaceCount():
|
||||
return 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
|
||||
wa /= wt
|
||||
wb /= wt
|
||||
wc /= wt
|
||||
texcoords = wa * ta + wb * tb + wc * tc
|
||||
return texcoords
|
||||
|
||||
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()
|
||||
|
||||
# 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.KeyPressEvent and cast(KeyEvent, event).key == KeyEvent.ShiftKey:
|
||||
return False
|
||||
|
||||
if event.type == Event.MouseReleaseEvent and self._controller.getToolsEnabled():
|
||||
if MouseEvent.LeftButton not in cast(MouseEvent, event).buttons:
|
||||
return False
|
||||
self._mouse_held = False
|
||||
self._last_text_coords = None
|
||||
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
|
||||
|
||||
paintview = controller.getActiveView()
|
||||
if paintview is None or paintview.getPluginId() != "PaintTool":
|
||||
return False
|
||||
paintview = cast(PaintView, paintview)
|
||||
|
||||
if not self._selection_pass:
|
||||
return False
|
||||
|
||||
camera = self._controller.getScene().getActiveCamera()
|
||||
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)
|
||||
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)
|
||||
|
||||
if not self._picking_pass:
|
||||
self._picking_pass = PickingPass(camera.getViewportWidth(), camera.getViewportHeight())
|
||||
self._picking_pass.render()
|
||||
|
||||
self._selection_pass.renderFacesMode()
|
||||
|
||||
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
|
180
plugins/PaintTool/PaintTool.qml
Normal file
180
plugins/PaintTool/PaintTool.qml
Normal file
@ -0,0 +1,180 @@
|
||||
// Copyright (c) 2025 UltiMaker
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import UM 1.7 as UM
|
||||
|
||||
Item
|
||||
{
|
||||
id: base
|
||||
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: 40
|
||||
value: 10
|
||||
|
||||
onPressedChanged: function(pressed)
|
||||
{
|
||||
if(! pressed)
|
||||
{
|
||||
UM.Controller.triggerActionWithData("setBrushSize", shapeSizeSlider.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
51
plugins/PaintTool/PaintView.py
Normal file
51
plugins/PaintTool/PaintView.py
Normal file
@ -0,0 +1,51 @@
|
||||
# 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
|
||||
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
|
||||
|
||||
# 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:
|
||||
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)
|
||||
|
||||
def getUvTexDimensions(self):
|
||||
return self._tex_width, self._tex_height
|
||||
|
||||
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())
|
30
plugins/PaintTool/__init__.py
Normal file
30
plugins/PaintTool/__init__.py
Normal file
@ -0,0 +1,30 @@
|
||||
# Copyright (c) 2025 UltiMaker
|
||||
# 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")
|
||||
|
||||
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):
|
||||
return {
|
||||
"tool": PaintTool.PaintTool(),
|
||||
"view": PaintView.PaintView()
|
||||
}
|
142
plugins/PaintTool/paint.shader
Normal file
142
plugins/PaintTool/paint.shader
Normal file
@ -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
|
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,13 @@ 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.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||
self._enabled_shader = None
|
||||
self._disabled_shader = None
|
||||
@ -212,7 +213,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)))
|
||||
|
Loading…
x
Reference in New Issue
Block a user