Skip to content

Commit

Permalink
Groups and merged meshes should also 'lay flat by face'.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
rburema committed Feb 28, 2024
1 parent 4f69e5b commit 0f4d438
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 13 deletions.
57 changes: 55 additions & 2 deletions UM/View/SelectionPass.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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

Expand Down Expand Up @@ -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():
Expand All @@ -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:
Expand All @@ -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()
Expand All @@ -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.)
)
Expand Down
12 changes: 6 additions & 6 deletions plugins/Tools/RotateTool/RotateTool.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 3 additions & 4 deletions plugins/Tools/SelectionTool/SelectionTool.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand Down
22 changes: 21 additions & 1 deletion resources/shaders/select_face.shader
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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.);
Expand All @@ -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.
}
Expand All @@ -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
2 changes: 2 additions & 0 deletions tests/Scene/TestSelection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 0f4d438

Please sign in to comment.