-
-
Notifications
You must be signed in to change notification settings - Fork 8
/
MeasureTool.py
341 lines (272 loc) · 12.6 KB
/
MeasureTool.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# Copyright (c) 2023 Aldo Hoeben / fieldOfView
# MeasureTool is released under the terms of the AGPLv3 or higher.
from UM.Tool import Tool
from UM.Event import Event, MouseEvent
from UM.Math.Vector import Vector
from UM.Scene.Selection import Selection
from UM.Scene.SceneNode import SceneNode
from UM.Logger import Logger
from UM.i18n import i18nCatalog
from UM.Resources import Resources
from UM.Version import Version
from UM.View.GL.OpenGL import OpenGL
from cura.CuraApplication import CuraApplication
from .MeasurePass import MeasurePass
from .MeasureToolHandle import MeasureToolHandle
try:
from cura.ApplicationMetadata import CuraSDKVersion
except ImportError: # Cura <= 3.6
CuraSDKVersion = "6.0.0"
if CuraSDKVersion >= "8.0.0":
from PyQt6.QtCore import Qt, QObject
from PyQt6.QtWidgets import QApplication
from PyQt6.QtGui import QVector3D
KeyboardShiftModifier = Qt.KeyboardModifier.ShiftModifier
else:
from PyQt5.QtCore import Qt, QObject
from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QVector3D
KeyboardShiftModifier = Qt.ShiftModifier
from math import inf
import os.path
from typing import cast, List, Optional
class MeasureTool(Tool):
def __init__(self, parent=None) -> None:
super().__init__()
self._application = CuraApplication.getInstance()
self._controller = self.getController()
self._measure_passes = [] # type: List[MeasurePass]
self._measure_passes_dirty = True
self._toolbutton_item = None # type: Optional[QObject]
self._tool_enabled = False
self._dragging = False
self._from_locked = False
self._snap_vertices = False
Resources.addSearchPath(os.path.abspath(os.path.join(
os.path.dirname(__file__),
"resources"
))) # Plugin translation file import
self._i18n_catalog = i18nCatalog("measuretool")
self._points = [QVector3D(), QVector3D()]
self._active_point = 0
self._handle = (
MeasureToolHandle()
) # type: MeasureToolHandle # Because for some reason MyPy thinks this variable contains Optional[ToolHandle].
self._handle.setTool(self)
self.setExposedProperties("PointA", "PointB", "Distance", "ActivePoint", "FromLocked", "SnapVerticesSupported", "SnapVertices")
self._application.engineCreatedSignal.connect(self._onEngineCreated)
Selection.selectionChanged.connect(self._onSelectionChanged)
self._controller.activeStageChanged.connect(self._onActiveStageChanged)
self._controller.activeToolChanged.connect(self._onActiveToolChanged)
self._controller.getScene().sceneChanged.connect(self._onSceneChanged)
self._selection_tool = None # type: Optional[Tool]
self._application.getPreferences().addPreference("measuretool/unit_factor", 1)
def resetPoints(self) -> None:
self._points = [QVector3D(), QVector3D()]
self.propertyChanged.emit()
self._controller.getScene().sceneChanged.emit(self._handle)
def getPointA(self) -> QVector3D:
return self._points[0]
def getPointB(self) -> QVector3D:
return self._points[1]
def getDistance(self) -> QVector3D:
return self._points[1] - self._points[0]
def getActivePoint(self) -> int:
return self._active_point
def setActivePoint(self, active_point: int) -> None:
if active_point != self._active_point:
self._active_point = active_point
self.propertyChanged.emit()
def getFromLocked(self) -> bool:
return self._from_locked
def setFromLocked(self, locked) -> None:
if locked != self._from_locked:
self._from_locked = locked
self._active_point = 1
self.propertyChanged.emit()
def getSnapVerticesSupported(self) -> bool:
# Use a dummy postfix, since an equal version with a postfix is considered smaller normally.
return Version(OpenGL.getInstance().getOpenGLVersion()) >= Version("4.1 dummy-postfix")
def getSnapVertices(self) -> bool:
return self._snap_vertices
def setSnapVertices(self, snap) -> None:
if snap != self._snap_vertices:
self._snap_vertices = snap
self._measure_passes_dirty = True
self.propertyChanged.emit()
def _onEngineCreated(self) -> None:
main_window = self._application.getMainWindow()
if not main_window:
return
self._toolbutton_item = self._findToolbarIcon(main_window.contentItem())
self._forceToolEnabled()
main_window.viewportRectChanged.connect(self._createPickingPass)
main_window.widthChanged.connect(self._createPickingPass)
main_window.heightChanged.connect(self._createPickingPass)
self.propertyChanged.emit()
def _onSelectionChanged(self) -> None:
if not self._toolbutton_item:
return
self._application.callLater(lambda: self._forceToolEnabled())
def _onActiveStageChanged(self) -> None:
self._tool_enabled = self._controller.getActiveStage().stageId == "PrepareStage"
if not self._tool_enabled:
self._controller.setSelectionTool(self._selection_tool or "SelectionTool")
self._selection_tool = None
if self._controller.getActiveTool() == self:
self._controller.setActiveTool(self._getFallbackTool())
self._forceToolEnabled()
def _onActiveToolChanged(self) -> None:
if self._controller.getActiveTool() != self:
self._controller.setSelectionTool(self._selection_tool or "SelectionTool")
self._selection_tool = None
def _onSceneChanged(self, node: SceneNode) -> None:
if node == self._handle:
return
self._measure_passes_dirty = True
def _findToolbarIcon(self, rootItem: QObject) -> Optional[QObject]:
for child in rootItem.childItems():
class_name = child.metaObject().className()
if class_name.startswith("ToolbarButton_QMLTYPE") and child.property(
"text"
) == self._i18n_catalog.i18nc("@label", "Measure"):
return child
elif (
class_name.startswith("QQuickItem")
or class_name.startswith("QQuickColumn")
or class_name.startswith("Toolbar_QMLTYPE")
):
found = self._findToolbarIcon(child)
if found:
return found
return None
def _forceToolEnabled(self, passive=False) -> None:
if not self._toolbutton_item:
return
try:
if self._tool_enabled:
self._toolbutton_item.setProperty("enabled", True)
if self._application._previous_active_tool == "MeasureTool" and not passive:
self._controller.setActiveTool(self._application._previous_active_tool)
else:
self._toolbutton_item.setProperty("enabled", False)
if self._controller.getActiveTool() == self and not passive:
self._controller.setActiveTool(self._getFallbackTool())
except RuntimeError:
Logger.log("w", "The toolbutton item seems to have gone missing; trying to find it back.")
main_window = self._application.getMainWindow()
if not main_window:
return
self._toolbutton_item = self._findToolbarIcon(main_window.contentItem())
def event(self, event: Event) -> bool:
result = super().event(event)
if not self._tool_enabled:
return result
# overridden from ToolHandle.event(), because we also want to show the handle when there is no selection
# disabling the tool oon Event.ToolDeactivateEvent is properly handled in ToolHandle.evemt()
if event.type == Event.ToolActivateEvent:
if self._handle:
self._handle.setParent(self.getController().getScene().getRoot())
self._handle.setEnabled(True)
self._selection_tool = self._controller._selection_tool
self._controller.setSelectionTool(None)
self._application.callLater(lambda: self._forceToolEnabled(passive=True))
if event.type == Event.ToolDeactivateEvent:
self._controller.setSelectionTool(self._selection_tool or "SelectionTool")
self._selection_tool = None
self._application.callLater(lambda: self._forceToolEnabled(passive=True))
if (
event.type == Event.MouseReleaseEvent
and MouseEvent.LeftButton in cast(MouseEvent, event).buttons
):
self._dragging = False
if (
event.type == Event.MousePressEvent
and MouseEvent.LeftButton in cast(MouseEvent, event).buttons
):
mouse_event = cast(MouseEvent, event)
if self._from_locked:
self._active_point = 1
elif QApplication.keyboardModifiers() & KeyboardShiftModifier:
if self._active_point == 0:
self._active_point = 1
else:
self._active_point = 0
else:
distances = [] # type: List[float]
camera = self._controller.getScene().getActiveCamera()
for point in self._points:
if camera.isPerspective():
projected_point = camera.project(
Vector(point.x(), point.y(), point.z())
)
else:
# Camera.project() does not work for orthographic views in Cura 4.9 and before, so we calculate our own projection
projection = camera.getProjectionMatrix()
view = camera.getWorldTransformation()
view.invert()
position = Vector(point.x(), point.y(), point.z())
position = position.preMultiply(view)
position = position.preMultiply(projection)
projected_point = (position.x, position.y)
dx = projected_point[0] - (
(
camera.getWindowSize()[0]
* (mouse_event.x + 1)
/ camera.getViewportWidth()
)
- 1
)
dy = projected_point[1] + mouse_event.y
distances.append(dx * dx + dy * dy)
self._active_point = 0
if distances[1] < distances[0]:
self._active_point = 1
self._dragging = True
result = self._handleMouseEvent(event, result)
if event.type == Event.MouseMoveEvent:
if self._dragging:
result = self._handleMouseEvent(event, result)
if self._selection_tool:
self._selection_tool.event(event)
return result
def _handleMouseEvent(self, event: Event, result: bool) -> bool:
if not self._measure_passes:
self._createPickingPass()
if not self._measure_passes:
return False
picked_coordinate = []
mouse_event = cast(MouseEvent, event)
for axis in self._measure_passes:
if self._measure_passes_dirty:
axis.render(self._snap_vertices)
axis_value = axis.getPickedCoordinate(mouse_event.x, mouse_event.y)
if axis_value == inf:
return False
picked_coordinate.append(axis_value)
self._measure_passes_dirty = False
self._points[self._active_point] = QVector3D(*picked_coordinate)
self._controller.getScene().sceneChanged.emit(self._handle)
self.propertyChanged.emit()
return result
def _createPickingPass(self, *args, **kwargs) -> None:
active_camera = self._controller.getScene().getActiveCamera()
if not active_camera:
return
viewport_width = active_camera.getViewportWidth()
viewport_height = active_camera.getViewportHeight()
self._measure_passes.clear()
try:
# Create a set of passes for picking a world-space location from the mouse location
for axis in range(0, 3):
self._measure_passes.append(
MeasurePass(viewport_width, viewport_height, axis)
)
except:
pass
self._measure_passes_dirty = True
def _getFallbackTool(self) -> str:
try:
return self._controller._fallback_tool
except AttributeError:
return "TranslateTool"