From 0f4d4386f19b96874a2bd3e8046450964c3ede50 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 28 Feb 2024 18:37:45 +0100 Subject: [PATCH 1/4] Groups and merged meshes should also 'lay flat by face'. Note that when this is the case (group or merged) the object selected for face node isn't the same as the 'overall' selected node in the scene. In that case, the overall object is the parent group or merged node, but the selected object 'for the face' needs to be one with a mesh directly attached to it (so a leaf in that tree). part of CURA-10149 --- UM/View/SelectionPass.py | 57 +++++++++++++++++++- plugins/Tools/RotateTool/RotateTool.py | 12 ++--- plugins/Tools/SelectionTool/SelectionTool.py | 7 ++- resources/shaders/select_face.shader | 22 +++++++- tests/Scene/TestSelection.py | 2 + 5 files changed, 87 insertions(+), 13 deletions(-) diff --git a/UM/View/SelectionPass.py b/UM/View/SelectionPass.py index 945b789776..4f345d5ee5 100644 --- a/UM/View/SelectionPass.py +++ b/UM/View/SelectionPass.py @@ -1,7 +1,8 @@ -# Copyright (c) 2020 Ultimaker B.V. +# Copyright (c) 2024 UltiMaker # Uranium is released under the terms of the LGPLv3 or higher. import enum +import math import random from typing import TYPE_CHECKING @@ -45,6 +46,7 @@ def __init__(self, width, height): self._renderer = Application.getInstance().getRenderer() self._selection_map = {} + self._face_mode_selection_map = [] self._default_toolhandle_selection_map = { self._dropAlpha(ToolHandle.DisabledSelectionColor): ToolHandle.NoAxis, self._dropAlpha(ToolHandle.XAxisSelectionColor): ToolHandle.XAxis, @@ -63,6 +65,7 @@ def __init__(self, width, height): self._mode = SelectionPass.SelectionMode.OBJECTS Selection.selectedFaceChanged.connect(self._onSelectedFaceChanged) + self._face_mode_max_objects = 1 # Needed when selecting a face for (a) grouped or merged object(s). self._output = None @@ -121,6 +124,10 @@ def _renderObjectsMode(self): def _renderFacesMode(self): batch = RenderBatch(self._face_shader) + self._face_mode_max_objects = 1 + self._face_shader.setUniformValue("u_modelId", 0) + self._face_shader.setUniformValue("u_maxModelId", self._face_mode_max_objects) + self._face_mode_selection_map = [] selectable_objects = False for node in Selection.getAllSelectedObjects(): @@ -130,6 +137,27 @@ def _renderFacesMode(self): if node.isSelectable() and node.getMeshData(): selectable_objects = True batch.addItem(transformation = node.getWorldTransformation(copy = False), mesh = node.getMeshData(), normal_transformation=node.getCachedNormalMatrix()) + self._face_mode_selection_map.append(node) + elif node.hasChildren(): + # Drill down to see if we're in a group or merged meshes type situation. + # This should be OK, as we should get both the mesh-id _and_ face-id from the rendering mesh. + self._face_mode_max_objects = sum([1 if (node.isSelectable() and node.getMeshData()) else 0 for node in node.getChildren()]) + current_model_id = 0 + for node in node.getChildren(): + if node.isSelectable() and node.getMeshData(): + selectable_objects = True + batch.addItem( + transformation = node.getWorldTransformation(copy = False), + mesh = node.getMeshData(), + uniforms = {"model_id": current_model_id, "max_model_id": self._face_mode_max_objects}, + normal_transformation = node.getCachedNormalMatrix()) + self._face_mode_selection_map.append(node) + current_model_id += 1 + if current_model_id >= 128: + break # Shader can't handle more than 128 (ids 0 through 127) objects in a group. + + if selectable_objects: + break # only one group allowed self.bind() if selectable_objects: @@ -152,6 +180,29 @@ def getIdAtPosition(self, x, y): pixel = output.pixel(px, py) return self._selection_map.get(Color.fromARGB(pixel), None) + def getIdAtPositionFaceMode(self, x, y): + """Get an unique identifier to any object currently selected for by-face manipulation at a pixel coordinate.""" + output = self.getOutput() + + window_size = self._renderer.getWindowSize() + + px = round((0.5 + x / 2.0) * window_size[0]) + py = round((0.5 + y / 2.0) * window_size[1]) + + if px < 0 or px > (output.width() - 1) or py < 0 or py > (output.height() - 1): + return None + + blue_channel = int(Color.fromARGB(output.pixel(px, py)).b * 255.) + if blue_channel % 2 == 0: # check signal (any selected object here) bit + return None + + max_objects_mask = int(math.pow(2, int(math.ceil(math.log2(self._face_mode_max_objects))) + 1)) - 1 + index = (blue_channel & max_objects_mask) >> 1 + if 0 <= index < len(self._face_mode_selection_map): + return self._face_mode_selection_map[index] + else: + return None + def getFaceIdAtPosition(self, x, y): """Get an unique identifier to the face of the polygon at a certain pixel-coordinate.""" output = self.getOutput() @@ -167,8 +218,10 @@ def getFaceIdAtPosition(self, x, y): face_color = Color.fromARGB(output.pixel(px, py)) if int(face_color.b * 255) % 2 == 0: return -1 + + max_objects_adjusted = int(math.ceil(math.log2(self._face_mode_max_objects))) + 1 return ( - ((int(face_color.b * 255.) - 1) << 15) | + ((int(face_color.b * 255.) >> max_objects_adjusted) << 15) | (int(face_color.g * 255.) << 8) | int(face_color.r * 255.) ) diff --git a/plugins/Tools/RotateTool/RotateTool.py b/plugins/Tools/RotateTool/RotateTool.py index febe3e4c27..463c78ae8a 100644 --- a/plugins/Tools/RotateTool/RotateTool.py +++ b/plugins/Tools/RotateTool/RotateTool.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 Ultimaker B.V. +# Copyright (c) 2024 UltiMaker # Uranium is released under the terms of the LGPLv3 or higher. from typing import Optional @@ -20,8 +20,7 @@ from UM.Scene.Selection import Selection from UM.Scene.ToolHandle import ToolHandle from UM.Tool import Tool -from UM.Version import Version -from UM.View.GL.OpenGL import OpenGL +from UM.View.GL.OpenGL import OpenGLContext try: from . import RotateToolHandle @@ -237,10 +236,12 @@ def _onSelectedFaceChanged(self): self._handle.setEnabled(not Selection.getFaceSelectMode()) selected_face = Selection.getSelectedFace() - if not Selection.getSelectedFace() or not (Selection.hasSelection() and Selection.getFaceSelectMode()): + if selected_face is None or not (Selection.hasSelection() and Selection.getFaceSelectMode()): return original_node, face_id = selected_face + if original_node is None: + return meshdata = original_node.getMeshDataTransformed() if not meshdata or face_id < 0: return @@ -285,8 +286,7 @@ def getSelectFaceSupported(self) -> bool: :return: True if it is supported, or False otherwise. """ - # 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") + return not OpenGLContext.isLegacyOpenGL() def getRotationSnap(self): """Get the state of the "snap rotation to N-degree increments" option diff --git a/plugins/Tools/SelectionTool/SelectionTool.py b/plugins/Tools/SelectionTool/SelectionTool.py index 0becf7b0d9..75c18e220b 100644 --- a/plugins/Tools/SelectionTool/SelectionTool.py +++ b/plugins/Tools/SelectionTool/SelectionTool.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 Ultimaker B.V. +# Copyright (c) 2024 UltiMaker # Uranium is released under the terms of the LGPLv3 or higher. from PyQt6 import QtCore, QtWidgets @@ -67,8 +67,6 @@ def event(self, event): self._selection_pass = self._renderer.getRenderPass("selection") self.checkModifierKeys(event) - #if event.type == MouseEvent.MouseMoveEvent and Selection.getFaceSelectMode(): - # return self._pixelHover(event) if event.type == MouseEvent.MousePressEvent and MouseEvent.LeftButton in event.buttons and self._controller.getToolsEnabled(): # Perform a selection operation if self._selection_mode == self.PixelSelectionMode: @@ -159,9 +157,10 @@ def _pixelSelection(self, event): return True else: if Selection.getFaceSelectMode(): + node_for_face = self._selection_pass.getIdAtPositionFaceMode(event.x, event.y) face_id = self._selection_pass.getFaceIdAtPosition(event.x, event.y) if face_id >= 0: - Selection.toggleFace(node, face_id) + Selection.toggleFace(node_for_face, face_id) else: Selection.clear() Selection.clearFace() diff --git a/resources/shaders/select_face.shader b/resources/shaders/select_face.shader index 9b5838aad9..6c03939563 100644 --- a/resources/shaders/select_face.shader +++ b/resources/shaders/select_face.shader @@ -1,8 +1,10 @@ [shaders] vertex = + // NOTE: These legacy shaders are compiled, but not used. Select-by-face isn't possible in legacy-render-mode. uniform highp mat4 u_modelMatrix; uniform highp mat4 u_viewMatrix; uniform highp mat4 u_projectionMatrix; + uniform highp int u_modelId; attribute highp vec4 a_vertex; @@ -12,6 +14,9 @@ vertex = } fragment = + // NOTE: These legacy shaders are compiled, but not used. Select-by-face isn't possible in legacy-render-mode. + uniform highp int u_maxModelId; // So the output can still be up to ~8M faces for an ungrouped object. + void main() { gl_FragColor = vec4(0., 0., 0., 1.); @@ -22,27 +27,40 @@ fragment = vertex41core = #version 410 + uniform highp mat4 u_modelMatrix; uniform highp mat4 u_viewMatrix; uniform highp mat4 u_projectionMatrix; + uniform highp int u_modelId; in highp vec4 a_vertex; + flat out highp int v_modelId; + void main() { gl_Position = u_projectionMatrix * u_viewMatrix * u_modelMatrix * a_vertex; + v_modelId = u_modelId; } fragment41core = #version 410 + + uniform highp int u_maxModelId; // So the output can still be up to ~8M faces for an ungrouped object. + flat in highp int v_modelId; + out vec4 frag_color; void main() { + int max_model_adjusted = int(exp2(int(ceil(log2(u_maxModelId))) + 1)); + int blue_part_face_id = (gl_PrimitiveID / 0x10000) % 0x80; + frag_color = vec4(0., 0., 0., 1.); frag_color.r = (gl_PrimitiveID % 0x100) / 255.; frag_color.g = ((gl_PrimitiveID / 0x100) % 0x100) / 255.; - frag_color.b = (0x1 + 2 * ((gl_PrimitiveID / 0x10000) % 0x80)) / 255.; + frag_color.b = (0x1 + 2 * v_modelId + max_model_adjusted * blue_part_face_id) / 255.; + // Don't use alpha for anything, as some faces may be behind others, an only the front one's value is desired. // There isn't any control over the background color, so a signal-bit is put into the blue byte. } @@ -53,6 +71,8 @@ fragment41core = u_modelMatrix = model_matrix u_viewMatrix = view_matrix u_projectionMatrix = projection_matrix +u_modelId = model_id +u_maxModelId = max_model_id [attributes] a_vertex = vertex diff --git a/tests/Scene/TestSelection.py b/tests/Scene/TestSelection.py index 302c8c69a4..be39ff8752 100644 --- a/tests/Scene/TestSelection.py +++ b/tests/Scene/TestSelection.py @@ -101,6 +101,8 @@ def test_toggleFace(self): assert Selection.getSelectedFace() == (node_1, 91) Selection.toggleFace(node_2, 92) assert Selection.getSelectedFace() == (node_2, 92) + Selection.toggleFace(node_1, 92) + assert Selection.getSelectedFace() == (node_1, 92) Selection.toggleFace(node_2, 93) assert Selection.getSelectedFace() == (node_2, 93) Selection.toggleFace(node_2, 93) From 67adf87e758a34b027c2bb02a2cd63e1e7367bb7 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 28 Feb 2024 22:01:19 +0100 Subject: [PATCH 2/4] Make deeper groups work (with lay flat by face). part of CURA-10149 --- UM/View/SelectionPass.py | 30 +++++++++++++++----------- plugins/Tools/RotateTool/RotateTool.py | 10 ++++----- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/UM/View/SelectionPass.py b/UM/View/SelectionPass.py index 4f345d5ee5..8ad213a574 100644 --- a/UM/View/SelectionPass.py +++ b/UM/View/SelectionPass.py @@ -143,18 +143,22 @@ def _renderFacesMode(self): # This should be OK, as we should get both the mesh-id _and_ face-id from the rendering mesh. self._face_mode_max_objects = sum([1 if (node.isSelectable() and node.getMeshData()) else 0 for node in node.getChildren()]) current_model_id = 0 - for node in node.getChildren(): - if node.isSelectable() and node.getMeshData(): - selectable_objects = True - batch.addItem( - transformation = node.getWorldTransformation(copy = False), - mesh = node.getMeshData(), - uniforms = {"model_id": current_model_id, "max_model_id": self._face_mode_max_objects}, - normal_transformation = node.getCachedNormalMatrix()) - self._face_mode_selection_map.append(node) - current_model_id += 1 - if current_model_id >= 128: - break # Shader can't handle more than 128 (ids 0 through 127) objects in a group. + node_list = [node] + while len(node_list) > 0: + for node in node_list.pop().getChildren(): + if node.isSelectable() and node.getMeshData(): + selectable_objects = True + batch.addItem( + transformation = node.getWorldTransformation(copy = False), + mesh = node.getMeshData(), + uniforms = {"model_id": current_model_id, "max_model_id": self._face_mode_max_objects}, + normal_transformation = node.getCachedNormalMatrix()) + self._face_mode_selection_map.append(node) + current_model_id += 1 + if current_model_id >= 128: + break # Shader can't handle more than 128 (ids 0 through 127) objects in a group. + elif node.callDecoration("isGroup"): + node_list.append(node) if selectable_objects: break # only one group allowed @@ -184,6 +188,8 @@ def getIdAtPositionFaceMode(self, x, y): """Get an unique identifier to any object currently selected for by-face manipulation at a pixel coordinate.""" output = self.getOutput() + output.save("C:/tmp_/faceid.png") + window_size = self._renderer.getWindowSize() px = round((0.5 + x / 2.0) * window_size[0]) diff --git a/plugins/Tools/RotateTool/RotateTool.py b/plugins/Tools/RotateTool/RotateTool.py index 463c78ae8a..40e5281da4 100644 --- a/plugins/Tools/RotateTool/RotateTool.py +++ b/plugins/Tools/RotateTool/RotateTool.py @@ -255,13 +255,11 @@ def _onSelectedFaceChanged(self): rotation_quaternion = Quaternion.rotationTo(face_normal_vector.normalized(), Vector(0.0, -1.0, 0.0)) operation = GroupedOperation() - current_node = None # type: Optional[SceneNode] - for node in Selection.getAllSelectedObjects(): - current_node = node + current_node = original_node + parent_node = current_node.getParent() + while parent_node and parent_node.callDecoration("isGroup"): + current_node = parent_node parent_node = current_node.getParent() - while parent_node and parent_node.callDecoration("isGroup"): - current_node = parent_node - parent_node = current_node.getParent() if current_node is None: return From e1fa5d4cdac622db7ee5dbbc351dcf4456cba68e Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 28 Feb 2024 22:05:20 +0100 Subject: [PATCH 3/4] Fix number of calls (tests). done as part of CURA-10149 --- tests/Scene/TestSelection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Scene/TestSelection.py b/tests/Scene/TestSelection.py index be39ff8752..d32c69b13c 100644 --- a/tests/Scene/TestSelection.py +++ b/tests/Scene/TestSelection.py @@ -113,7 +113,7 @@ def test_toggleFace(self): Selection.clearFace() assert Selection.getSelectedFace() is None - assert Selection.selectedFaceChanged.emit.call_count == 6 + assert Selection.selectedFaceChanged.emit.call_count == 7 def test_hoverFace(self): Selection.hoverFaceChanged = MagicMock() From cebf409c04a20660fa5c008af0c8e28497d4c48d Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Thu, 29 Feb 2024 17:06:27 +0100 Subject: [PATCH 4/4] Remove debugging code. done as part of CURA-10149 --- UM/View/SelectionPass.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/UM/View/SelectionPass.py b/UM/View/SelectionPass.py index 8ad213a574..2dcec9491c 100644 --- a/UM/View/SelectionPass.py +++ b/UM/View/SelectionPass.py @@ -188,8 +188,6 @@ def getIdAtPositionFaceMode(self, x, y): """Get an unique identifier to any object currently selected for by-face manipulation at a pixel coordinate.""" output = self.getOutput() - output.save("C:/tmp_/faceid.png") - window_size = self._renderer.getWindowSize() px = round((0.5 + x / 2.0) * window_size[0])