mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-08-15 08:15:58 +08:00
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
This commit is contained in:
parent
93694e2da4
commit
19ea88a8ce
135
plugins/PaintTool/PaintTool.py
Normal file
135
plugins/PaintTool/PaintTool.py
Normal file
@ -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
|
14
plugins/PaintTool/PaintTool.qml
Normal file
14
plugins/PaintTool/PaintTool.qml
Normal file
@ -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"}
|
||||||
|
}
|
21
plugins/PaintTool/__init__.py
Normal file
21
plugins/PaintTool/__init__.py
Normal file
@ -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() }
|
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"
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user