mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-25 07:33:57 -06:00
Merge pull request #3492 from fieldOfView/feature_support_eraser_ux
Support Eraser improvements
This commit is contained in:
commit
f8283352c4
3 changed files with 308 additions and 34 deletions
69
cura/PickingPass.py
Normal file
69
cura/PickingPass.py
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.Math.Vector import Vector
|
||||||
|
from UM.Resources import Resources
|
||||||
|
|
||||||
|
from UM.View.RenderPass import RenderPass
|
||||||
|
from UM.View.GL.OpenGL import OpenGL
|
||||||
|
from UM.View.RenderBatch import RenderBatch
|
||||||
|
|
||||||
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
|
|
||||||
|
|
||||||
|
## A RenderPass subclass that renders a the distance of selectable objects from the active camera to a texture.
|
||||||
|
# The texture is used to map a 2d location (eg the mouse location) to a world space position
|
||||||
|
#
|
||||||
|
# Note that in order to increase precision, the 24 bit depth value is encoded into all three of the R,G & B channels
|
||||||
|
class PickingPass(RenderPass):
|
||||||
|
def __init__(self, width: int, height: int):
|
||||||
|
super().__init__("picking", width, height)
|
||||||
|
|
||||||
|
self._renderer = Application.getInstance().getRenderer()
|
||||||
|
|
||||||
|
self._shader = None
|
||||||
|
self._scene = Application.getInstance().getController().getScene()
|
||||||
|
|
||||||
|
def render(self) -> None:
|
||||||
|
if not self._shader:
|
||||||
|
self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "camera_distance.shader"))
|
||||||
|
|
||||||
|
width, height = self.getSize()
|
||||||
|
self._gl.glViewport(0, 0, width, height)
|
||||||
|
self._gl.glClearColor(1.0, 1.0, 1.0, 0.0)
|
||||||
|
self._gl.glClear(self._gl.GL_COLOR_BUFFER_BIT | self._gl.GL_DEPTH_BUFFER_BIT)
|
||||||
|
|
||||||
|
# Create a new batch to be rendered
|
||||||
|
batch = RenderBatch(self._shader)
|
||||||
|
|
||||||
|
# Fill up the batch with objects that can be sliced. `
|
||||||
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
|
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
|
||||||
|
batch.addItem(node.getWorldTransformation(), node.getMeshData())
|
||||||
|
|
||||||
|
self.bind()
|
||||||
|
batch.render(self._scene.getActiveCamera())
|
||||||
|
self.release()
|
||||||
|
|
||||||
|
## Get the distance in mm from the camera to at a certain pixel coordinate.
|
||||||
|
def getPickedDepth(self, x: int, y: int) -> float:
|
||||||
|
output = self.getOutput()
|
||||||
|
|
||||||
|
window_size = self._renderer.getWindowSize()
|
||||||
|
|
||||||
|
px = (0.5 + x / 2.0) * window_size[0]
|
||||||
|
py = (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 -1
|
||||||
|
|
||||||
|
distance = output.pixel(px, py) # distance in micron, from in r, g & b channels
|
||||||
|
distance = (distance & 0x00ffffff) / 1000. # drop the alpha channel and covert to mm
|
||||||
|
return distance
|
||||||
|
|
||||||
|
## Get the world coordinates of a picked point
|
||||||
|
def getPickedPosition(self, x: int, y: int) -> Vector:
|
||||||
|
distance = self.getPickedDepth(x, y)
|
||||||
|
ray = self._scene.getActiveCamera().getRay(x, y)
|
||||||
|
|
||||||
|
return ray.getPointAlongRay(distance)
|
|
@ -1,39 +1,96 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from UM.Math.Vector import Vector
|
|
||||||
from UM.Tool import Tool
|
|
||||||
from PyQt5.QtCore import Qt, QUrl
|
|
||||||
from UM.Application import Application
|
|
||||||
from UM.Event import Event
|
|
||||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
|
||||||
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
|
||||||
from UM.Settings.SettingInstance import SettingInstance
|
|
||||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
|
||||||
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
|
||||||
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
|
||||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt, QTimer
|
||||||
|
|
||||||
|
from UM.Math.Vector import Vector
|
||||||
|
from UM.Tool import Tool
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.Event import Event, MouseEvent
|
||||||
|
|
||||||
|
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||||
|
from UM.Scene.Selection import Selection
|
||||||
|
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||||
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
|
|
||||||
|
from cura.PickingPass import PickingPass
|
||||||
|
|
||||||
|
from UM.Operations.GroupedOperation import GroupedOperation
|
||||||
|
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
||||||
|
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
||||||
|
from cura.Operations.SetParentOperation import SetParentOperation
|
||||||
|
|
||||||
|
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||||
|
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||||
|
from UM.Scene.GroupDecorator import GroupDecorator
|
||||||
|
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||||
|
|
||||||
|
from UM.Settings.SettingInstance import SettingInstance
|
||||||
|
|
||||||
class SupportEraser(Tool):
|
class SupportEraser(Tool):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._shortcut_key = Qt.Key_G
|
self._shortcut_key = Qt.Key_G
|
||||||
self._controller = Application.getInstance().getController()
|
self._controller = Application.getInstance().getController()
|
||||||
|
|
||||||
|
self._selection_pass = None
|
||||||
|
Application.getInstance().globalContainerStackChanged.connect(self._updateEnabled)
|
||||||
|
|
||||||
|
# Note: if the selection is cleared with this tool active, there is no way to switch to
|
||||||
|
# another tool than to reselect an object (by clicking it) because the tool buttons in the
|
||||||
|
# toolbar will have been disabled. That is why we need to ignore the first press event
|
||||||
|
# after the selection has been cleared.
|
||||||
|
Selection.selectionChanged.connect(self._onSelectionChanged)
|
||||||
|
self._had_selection = False
|
||||||
|
self._skip_press = False
|
||||||
|
|
||||||
|
self._had_selection_timer = QTimer()
|
||||||
|
self._had_selection_timer.setInterval(0)
|
||||||
|
self._had_selection_timer.setSingleShot(True)
|
||||||
|
self._had_selection_timer.timeout.connect(self._selectionChangeDelay)
|
||||||
|
|
||||||
def event(self, event):
|
def event(self, event):
|
||||||
super().event(event)
|
super().event(event)
|
||||||
|
|
||||||
if event.type == Event.ToolActivateEvent:
|
if event.type == Event.MousePressEvent and self._controller.getToolsEnabled():
|
||||||
|
if self._skip_press:
|
||||||
|
# The selection was previously cleared, do not add/remove an anti-support mesh but
|
||||||
|
# use this click for selection and reactivating this tool only.
|
||||||
|
self._skip_press = False
|
||||||
|
return
|
||||||
|
|
||||||
# Load the remover mesh:
|
if self._selection_pass is None:
|
||||||
self._createEraserMesh()
|
# The selection renderpass is used to identify objects in the current view
|
||||||
|
self._selection_pass = Application.getInstance().getRenderer().getRenderPass("selection")
|
||||||
|
picked_node = self._controller.getScene().findObject(self._selection_pass.getIdAtPosition(event.x, event.y))
|
||||||
|
if not picked_node:
|
||||||
|
# There is no slicable object at the picked location
|
||||||
|
return
|
||||||
|
|
||||||
# After we load the mesh, deactivate the tool again:
|
node_stack = picked_node.callDecoration("getStack")
|
||||||
self.getController().setActiveTool(None)
|
if node_stack:
|
||||||
|
if node_stack.getProperty("anti_overhang_mesh", "value"):
|
||||||
|
self._removeEraserMesh(picked_node)
|
||||||
|
return
|
||||||
|
|
||||||
def _createEraserMesh(self):
|
elif node_stack.getProperty("support_mesh", "value") or node_stack.getProperty("infill_mesh", "value") or node_stack.getProperty("cutting_mesh", "value"):
|
||||||
|
# Only "normal" meshes can have anti_overhang_meshes added to them
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create a pass for picking a world-space location from the mouse location
|
||||||
|
active_camera = self._controller.getScene().getActiveCamera()
|
||||||
|
picking_pass = PickingPass(active_camera.getViewportWidth(), active_camera.getViewportHeight())
|
||||||
|
picking_pass.render()
|
||||||
|
|
||||||
|
picked_position = picking_pass.getPickedPosition(event.x, event.y)
|
||||||
|
|
||||||
|
# Add the anti_overhang_mesh cube at the picked location
|
||||||
|
self._createEraserMesh(picked_node, picked_position)
|
||||||
|
|
||||||
|
def _createEraserMesh(self, parent: CuraSceneNode, position: Vector):
|
||||||
node = CuraSceneNode()
|
node = CuraSceneNode()
|
||||||
|
|
||||||
node.setName("Eraser")
|
node.setName("Eraser")
|
||||||
|
@ -41,9 +98,7 @@ class SupportEraser(Tool):
|
||||||
mesh = MeshBuilder()
|
mesh = MeshBuilder()
|
||||||
mesh.addCube(10,10,10)
|
mesh.addCube(10,10,10)
|
||||||
node.setMeshData(mesh.build())
|
node.setMeshData(mesh.build())
|
||||||
# Place the cube in the platform. Do it manually so it works if the "automatic drop models" is OFF
|
node.setPosition(position)
|
||||||
move_vector = Vector(0, 5, 0)
|
|
||||||
node.setPosition(move_vector)
|
|
||||||
|
|
||||||
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
||||||
|
|
||||||
|
@ -51,21 +106,88 @@ class SupportEraser(Tool):
|
||||||
node.addDecorator(BuildPlateDecorator(active_build_plate))
|
node.addDecorator(BuildPlateDecorator(active_build_plate))
|
||||||
node.addDecorator(SliceableObjectDecorator())
|
node.addDecorator(SliceableObjectDecorator())
|
||||||
|
|
||||||
stack = node.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
|
stack = node.callDecoration("getStack") # created by SettingOverrideDecorator
|
||||||
if not stack:
|
|
||||||
node.addDecorator(SettingOverrideDecorator())
|
|
||||||
stack = node.callDecoration("getStack")
|
|
||||||
|
|
||||||
settings = stack.getTop()
|
settings = stack.getTop()
|
||||||
|
|
||||||
if not (settings.getInstance("anti_overhang_mesh") and settings.getProperty("anti_overhang_mesh", "value")):
|
definition = stack.getSettingDefinition("anti_overhang_mesh")
|
||||||
definition = stack.getSettingDefinition("anti_overhang_mesh")
|
new_instance = SettingInstance(definition, settings)
|
||||||
new_instance = SettingInstance(definition, settings)
|
new_instance.setProperty("value", True)
|
||||||
new_instance.setProperty("value", True)
|
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
||||||
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
settings.addInstance(new_instance)
|
||||||
settings.addInstance(new_instance)
|
|
||||||
|
|
||||||
scene = self._controller.getScene()
|
root = self._controller.getScene().getRoot()
|
||||||
op = AddSceneNodeOperation(node, scene.getRoot())
|
|
||||||
|
op = GroupedOperation()
|
||||||
|
# First add the node to the scene, so it gets the expected transform
|
||||||
|
op.addOperation(AddSceneNodeOperation(node, root))
|
||||||
|
|
||||||
|
# Determine the parent group the node should be put in
|
||||||
|
if parent.getParent().callDecoration("isGroup"):
|
||||||
|
group = parent.getParent()
|
||||||
|
else:
|
||||||
|
# Create a group-node
|
||||||
|
group = CuraSceneNode()
|
||||||
|
group.addDecorator(GroupDecorator())
|
||||||
|
group.addDecorator(BuildPlateDecorator(active_build_plate))
|
||||||
|
group.setParent(root)
|
||||||
|
center = parent.getPosition()
|
||||||
|
group.setPosition(center)
|
||||||
|
group.setCenterPosition(center)
|
||||||
|
op.addOperation(SetParentOperation(parent, group))
|
||||||
|
|
||||||
|
op.addOperation(SetParentOperation(node, group))
|
||||||
op.push()
|
op.push()
|
||||||
Application.getInstance().getController().getScene().sceneChanged.emit(node)
|
Application.getInstance().getController().getScene().sceneChanged.emit(node)
|
||||||
|
|
||||||
|
# Select the picked node so the group does not get drawn as a wireframe (yet)
|
||||||
|
if not Selection.isSelected(parent):
|
||||||
|
Selection.add(parent)
|
||||||
|
if Selection.isSelected(group):
|
||||||
|
Selection.remove(group)
|
||||||
|
|
||||||
|
def _removeEraserMesh(self, node: CuraSceneNode):
|
||||||
|
group = node.getParent()
|
||||||
|
if group.callDecoration("isGroup"):
|
||||||
|
parent = group.getChildren()[0]
|
||||||
|
|
||||||
|
op = GroupedOperation()
|
||||||
|
op.addOperation(RemoveSceneNodeOperation(node))
|
||||||
|
if len(group.getChildren()) == 2:
|
||||||
|
op.addOperation(SetParentOperation(parent, group.getParent()))
|
||||||
|
|
||||||
|
op.push()
|
||||||
|
Application.getInstance().getController().getScene().sceneChanged.emit(node)
|
||||||
|
|
||||||
|
# Select the picked node so the group does not get drawn as a wireframe (yet)
|
||||||
|
if parent and not Selection.isSelected(parent):
|
||||||
|
Selection.add(parent)
|
||||||
|
if Selection.isSelected(group):
|
||||||
|
Selection.remove(group)
|
||||||
|
|
||||||
|
def _updateEnabled(self):
|
||||||
|
plugin_enabled = False
|
||||||
|
|
||||||
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if global_container_stack:
|
||||||
|
plugin_enabled = global_container_stack.getProperty("anti_overhang_mesh", "enabled")
|
||||||
|
|
||||||
|
Application.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, plugin_enabled)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _onSelectionChanged(self):
|
||||||
|
# When selection is passed from one object to another object, first the selection is cleared
|
||||||
|
# and then it is set to the new object. We are only interested in the change from no selection
|
||||||
|
# to a selection or vice-versa, not in a change from one object to another. A timer is used to
|
||||||
|
# "merge" a possible clear/select action in a single frame
|
||||||
|
if Selection.hasSelection() != self._had_selection:
|
||||||
|
self._had_selection_timer.start()
|
||||||
|
|
||||||
|
def _selectionChangeDelay(self):
|
||||||
|
has_selection = Selection.hasSelection()
|
||||||
|
if not has_selection and self._had_selection:
|
||||||
|
self._skip_press = True
|
||||||
|
else:
|
||||||
|
self._skip_press = False
|
||||||
|
|
||||||
|
self._had_selection = has_selection
|
||||||
|
|
83
resources/shaders/camera_distance.shader
Normal file
83
resources/shaders/camera_distance.shader
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
[shaders]
|
||||||
|
vertex =
|
||||||
|
uniform highp mat4 u_modelMatrix;
|
||||||
|
uniform highp mat4 u_viewProjectionMatrix;
|
||||||
|
|
||||||
|
attribute highp vec4 a_vertex;
|
||||||
|
|
||||||
|
varying highp vec3 v_vertex;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
vec4 world_space_vert = u_modelMatrix * a_vertex;
|
||||||
|
gl_Position = u_viewProjectionMatrix * world_space_vert;
|
||||||
|
|
||||||
|
v_vertex = world_space_vert.xyz;
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment =
|
||||||
|
uniform highp vec3 u_viewPosition;
|
||||||
|
|
||||||
|
varying highp vec3 v_vertex;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
highp float distance_to_camera = distance(v_vertex, u_viewPosition) * 1000.; // distance in micron
|
||||||
|
|
||||||
|
vec3 encoded; // encode float into 3 8-bit channels; this gives a precision of a micron at a range of up to ~16 meter
|
||||||
|
encoded.b = floor(distance_to_camera / 65536.0);
|
||||||
|
encoded.g = floor((distance_to_camera - encoded.b * 65536.0) / 256.0);
|
||||||
|
encoded.r = floor(distance_to_camera - encoded.b * 65536.0 - encoded.g * 256.0);
|
||||||
|
|
||||||
|
gl_FragColor.rgb = encoded / 255.;
|
||||||
|
gl_FragColor.a = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
vertex41core =
|
||||||
|
#version 410
|
||||||
|
uniform highp mat4 u_modelMatrix;
|
||||||
|
uniform highp mat4 u_viewProjectionMatrix;
|
||||||
|
|
||||||
|
in highp vec4 a_vertex;
|
||||||
|
|
||||||
|
out highp vec3 v_vertex;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
vec4 world_space_vert = u_modelMatrix * a_vertex;
|
||||||
|
gl_Position = u_viewProjectionMatrix * world_space_vert;
|
||||||
|
|
||||||
|
v_vertex = world_space_vert.xyz;
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment41core =
|
||||||
|
#version 410
|
||||||
|
uniform highp vec3 u_viewPosition;
|
||||||
|
|
||||||
|
in highp vec3 v_vertex;
|
||||||
|
|
||||||
|
out vec4 frag_color;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
highp float distance_to_camera = distance(v_vertex, u_viewPosition) * 1000.; // distance in micron
|
||||||
|
|
||||||
|
vec3 encoded; // encode float into 3 8-bit channels; this gives a precision of a micron at a range of up to ~16 meter
|
||||||
|
encoded.r = floor(distance_to_camera / 65536.0);
|
||||||
|
encoded.g = floor((distance_to_camera - encoded.r * 65536.0) / 256.0);
|
||||||
|
encoded.b = floor(distance_to_camera - encoded.r * 65536.0 - encoded.g * 256.0);
|
||||||
|
|
||||||
|
frag_color.rgb = encoded / 255.;
|
||||||
|
frag_color.a = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[defaults]
|
||||||
|
|
||||||
|
[bindings]
|
||||||
|
u_modelMatrix = model_matrix
|
||||||
|
u_viewProjectionMatrix = view_projection_matrix
|
||||||
|
u_normalMatrix = normal_matrix
|
||||||
|
u_viewPosition = view_position
|
||||||
|
|
||||||
|
[attributes]
|
||||||
|
a_vertex = vertex
|
Loading…
Add table
Add a link
Reference in a new issue