diff --git a/cura/PickingPass.py b/cura/PickingPass.py new file mode 100644 index 0000000000..2a1abe8f63 --- /dev/null +++ b/cura/PickingPass.py @@ -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) diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 8b3ad0f4dd..58624ea058 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -1,39 +1,96 @@ # Copyright (c) 2018 Ultimaker B.V. # 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.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): def __init__(self): super().__init__() self._shortcut_key = Qt.Key_G 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): 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: - self._createEraserMesh() + if self._selection_pass is None: + # 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: - self.getController().setActiveTool(None) + node_stack = picked_node.callDecoration("getStack") + 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.setName("Eraser") @@ -41,9 +98,7 @@ class SupportEraser(Tool): mesh = MeshBuilder() mesh.addCube(10,10,10) node.setMeshData(mesh.build()) - # Place the cube in the platform. Do it manually so it works if the "automatic drop models" is OFF - move_vector = Vector(0, 5, 0) - node.setPosition(move_vector) + node.setPosition(position) active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate @@ -51,21 +106,88 @@ class SupportEraser(Tool): node.addDecorator(BuildPlateDecorator(active_build_plate)) node.addDecorator(SliceableObjectDecorator()) - stack = node.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway. - if not stack: - node.addDecorator(SettingOverrideDecorator()) - stack = node.callDecoration("getStack") - + stack = node.callDecoration("getStack") # created by SettingOverrideDecorator settings = stack.getTop() - if not (settings.getInstance("anti_overhang_mesh") and settings.getProperty("anti_overhang_mesh", "value")): - definition = stack.getSettingDefinition("anti_overhang_mesh") - new_instance = SettingInstance(definition, settings) - new_instance.setProperty("value", True) - new_instance.resetState() # Ensure that the state is not seen as a user state. - settings.addInstance(new_instance) + definition = stack.getSettingDefinition("anti_overhang_mesh") + new_instance = SettingInstance(definition, settings) + new_instance.setProperty("value", True) + new_instance.resetState() # Ensure that the state is not seen as a user state. + settings.addInstance(new_instance) - scene = self._controller.getScene() - op = AddSceneNodeOperation(node, scene.getRoot()) + root = self._controller.getScene().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() 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 diff --git a/resources/shaders/camera_distance.shader b/resources/shaders/camera_distance.shader new file mode 100644 index 0000000000..e6e894a2f6 --- /dev/null +++ b/resources/shaders/camera_distance.shader @@ -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