Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CURA-10149] make 'Lay Flat By Face' work for groups & merges #942

Merged
merged 4 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 61 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,31 @@ 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
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

self.bind()
if selectable_objects:
Expand All @@ -152,6 +184,31 @@ 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()

output.save("C:/tmp_/faceid.png")

rburema marked this conversation as resolved.
Show resolved Hide resolved
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 +224,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
22 changes: 10 additions & 12 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 All @@ -254,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

Expand All @@ -285,8 +284,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
4 changes: 3 additions & 1 deletion 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 All @@ -111,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()
Expand Down
Loading