From 0caea24afcb8d94fbf32ef330a0d2cc6788be16f Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 13 Mar 2018 11:54:32 +0100 Subject: [PATCH 01/12] Add depth pass for picking a location --- cura/DepthPass.py | 60 +++++++++++++++++ plugins/SupportEraser/SupportEraser.py | 27 +++++--- resources/shaders/camera_distance.shader | 83 ++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 10 deletions(-) create mode 100644 cura/DepthPass.py create mode 100644 resources/shaders/camera_distance.shader diff --git a/cura/DepthPass.py b/cura/DepthPass.py new file mode 100644 index 0000000000..3fb57b0341 --- /dev/null +++ b/cura/DepthPass.py @@ -0,0 +1,60 @@ +# 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.Color import Color +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 depthmap of selectable objects to a texture. +# It uses the active camera by default, but it can be overridden to use a different camera. +# +# Note that in order to increase precision, the 24 bit depth value is encoded into all three of the R,G & B channels +class DepthPass(RenderPass): + def __init__(self, width: int, height: int): + super().__init__("preview", width, height, 0) + + 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")) + + self._gl.glClearColor(0.0, 0.0, 0.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 getDepthAtPosition(self, x, y): + 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 None + + distance = output.pixel(px, py) # distance in micron, from in r, g & b channels + return distance / 1000. diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 8b3ad0f4dd..119e598886 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -4,14 +4,16 @@ 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.Event import Event, MouseEvent 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 UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator +from cura.DepthPass import DepthPass import os import os.path @@ -25,15 +27,22 @@ class SupportEraser(Tool): def event(self, event): super().event(event) - if event.type == Event.ToolActivateEvent: + if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(): + active_camera = self._controller.getScene().getActiveCamera() - # Load the remover mesh: - self._createEraserMesh() + # Create depth pass for picking + render_width, render_height = active_camera.getWindowSize() + depth_pass = DepthPass(int(render_width), int(render_height)) + depth_pass.render() - # After we load the mesh, deactivate the tool again: - self.getController().setActiveTool(None) + distance = depth_pass.getDepthAtPosition(event.x, event.y) + ray = active_camera.getRay(event.x, event.y) + picked_position = ray.getPointAlongRay(distance) - def _createEraserMesh(self): + # Add the anto_overhang_mesh cube: + self._createEraserMesh(picked_position) + + def _createEraserMesh(self, position: Vector): node = CuraSceneNode() node.setName("Eraser") @@ -41,9 +50,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 diff --git a/resources/shaders/camera_distance.shader b/resources/shaders/camera_distance.shader new file mode 100644 index 0000000000..2dd90e7f15 --- /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.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); + + 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 From 73558c9e3695335008cda045c4293e837e1ef3f0 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 13 Mar 2018 19:44:53 +0100 Subject: [PATCH 02/12] Fix rendering depth pass --- cura/DepthPass.py | 9 ++++++--- plugins/SupportEraser/SupportEraser.py | 3 +-- resources/shaders/camera_distance.shader | 6 +++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/cura/DepthPass.py b/cura/DepthPass.py index 3fb57b0341..a2cfdbaab0 100644 --- a/cura/DepthPass.py +++ b/cura/DepthPass.py @@ -17,7 +17,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator # Note that in order to increase precision, the 24 bit depth value is encoded into all three of the R,G & B channels class DepthPass(RenderPass): def __init__(self, width: int, height: int): - super().__init__("preview", width, height, 0) + super().__init__("depth", width, height) self._renderer = Application.getInstance().getRenderer() @@ -29,7 +29,9 @@ class DepthPass(RenderPass): if not self._shader: self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "camera_distance.shader")) - self._gl.glClearColor(0.0, 0.0, 0.0, 0.0) + 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 @@ -57,4 +59,5 @@ class DepthPass(RenderPass): return None distance = output.pixel(px, py) # distance in micron, from in r, g & b channels - return distance / 1000. + distance = (distance & 0x00ffffff) / 1000. # drop the alpha channel and covert to mm + return distance diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 119e598886..4034b524b8 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -31,8 +31,7 @@ class SupportEraser(Tool): active_camera = self._controller.getScene().getActiveCamera() # Create depth pass for picking - render_width, render_height = active_camera.getWindowSize() - depth_pass = DepthPass(int(render_width), int(render_height)) + depth_pass = DepthPass(active_camera.getViewportWidth(), active_camera.getViewportHeight()) depth_pass.render() distance = depth_pass.getDepthAtPosition(event.x, event.y) diff --git a/resources/shaders/camera_distance.shader b/resources/shaders/camera_distance.shader index 2dd90e7f15..e6e894a2f6 100644 --- a/resources/shaders/camera_distance.shader +++ b/resources/shaders/camera_distance.shader @@ -63,9 +63,9 @@ fragment41core = 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); + 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; From d88724aff539748dbdb0001bacbe6785cae68ba1 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 13 Mar 2018 20:05:49 +0100 Subject: [PATCH 03/12] Move ray picking to DepthPass --- cura/DepthPass.py | 15 +++++++++++---- plugins/SupportEraser/SupportEraser.py | 4 +--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/cura/DepthPass.py b/cura/DepthPass.py index a2cfdbaab0..4435d533ff 100644 --- a/cura/DepthPass.py +++ b/cura/DepthPass.py @@ -1,7 +1,7 @@ # 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.Color import Color +from UM.Math.Vector import Vector from UM.Resources import Resources from UM.View.RenderPass import RenderPass @@ -11,8 +11,8 @@ from UM.View.RenderBatch import RenderBatch from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator -## A RenderPass subclass that renders a depthmap of selectable objects to a texture. -# It uses the active camera by default, but it can be overridden to use a different camera. +## 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 DepthPass(RenderPass): @@ -47,7 +47,7 @@ class DepthPass(RenderPass): self.release() ## Get the distance in mm from the camera to at a certain pixel coordinate. - def getDepthAtPosition(self, x, y): + def getPickedDepth(self, x, y) -> float: output = self.getOutput() window_size = self._renderer.getWindowSize() @@ -61,3 +61,10 @@ class DepthPass(RenderPass): 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, y) -> 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 4034b524b8..0ddfed0cf1 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -34,9 +34,7 @@ class SupportEraser(Tool): depth_pass = DepthPass(active_camera.getViewportWidth(), active_camera.getViewportHeight()) depth_pass.render() - distance = depth_pass.getDepthAtPosition(event.x, event.y) - ray = active_camera.getRay(event.x, event.y) - picked_position = ray.getPointAlongRay(distance) + picked_position = depth_pass.getPickedPosition(event.x, event.y) # Add the anto_overhang_mesh cube: self._createEraserMesh(picked_position) From a536da503bf374f5db0fec5180e9989294f77c22 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 13 Mar 2018 20:40:41 +0100 Subject: [PATCH 04/12] Rename DepthPass to PickingPass The map created by the shader is not strictly a depth map; not only is the "depth" encoded in the rgb channels, but it is also a distance to the camera instead of a "scene depth". --- cura/{DepthPass.py => PickingPass.py} | 4 ++-- plugins/SupportEraser/SupportEraser.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) rename cura/{DepthPass.py => PickingPass.py} (97%) diff --git a/cura/DepthPass.py b/cura/PickingPass.py similarity index 97% rename from cura/DepthPass.py rename to cura/PickingPass.py index 4435d533ff..4bd893e926 100644 --- a/cura/DepthPass.py +++ b/cura/PickingPass.py @@ -15,9 +15,9 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator # 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 DepthPass(RenderPass): +class PickingPass(RenderPass): def __init__(self, width: int, height: int): - super().__init__("depth", width, height) + super().__init__("picking", width, height) self._renderer = Application.getInstance().getRenderer() diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 0ddfed0cf1..35713805bc 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -13,7 +13,7 @@ from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator from cura.Scene.BuildPlateDecorator import BuildPlateDecorator from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator -from cura.DepthPass import DepthPass +from cura.PickingPass import PickingPass import os import os.path @@ -30,13 +30,13 @@ class SupportEraser(Tool): if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(): active_camera = self._controller.getScene().getActiveCamera() - # Create depth pass for picking - depth_pass = DepthPass(active_camera.getViewportWidth(), active_camera.getViewportHeight()) - depth_pass.render() + # Create a pass for picking a world-space location from the mouse location + picking_pass = PickingPass(active_camera.getViewportWidth(), active_camera.getViewportHeight()) + picking_pass.render() - picked_position = depth_pass.getPickedPosition(event.x, event.y) + picked_position = picking_pass.getPickedPosition(event.x, event.y) - # Add the anto_overhang_mesh cube: + # Add the anti_overhang_mesh cube at the picked location self._createEraserMesh(picked_position) def _createEraserMesh(self, position: Vector): From 7e4cb1c36ecb4aed44bc9da4aab676eed21e1ae3 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Sun, 11 Mar 2018 13:06:30 +0100 Subject: [PATCH 05/12] Disable Support Eraser if anti_overhang_mesh is disabled --- plugins/SupportEraser/SupportEraser.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 35713805bc..65d22bcdfd 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -24,6 +24,8 @@ class SupportEraser(Tool): self._shortcut_key = Qt.Key_G self._controller = Application.getInstance().getController() + Application.getInstance().globalContainerStackChanged.connect(self._updateEnabled) + def event(self, event): super().event(event) @@ -73,3 +75,12 @@ class SupportEraser(Tool): op = AddSceneNodeOperation(node, scene.getRoot()) op.push() Application.getInstance().getController().getScene().sceneChanged.emit(node) + + 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) From e4a416258b96c2927adba04d0c8ffd89abc3a8a7 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 15 Mar 2018 09:03:50 +0100 Subject: [PATCH 06/12] Fix code-style and type hinting --- cura/PickingPass.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cura/PickingPass.py b/cura/PickingPass.py index 4bd893e926..2a1abe8f63 100644 --- a/cura/PickingPass.py +++ b/cura/PickingPass.py @@ -24,7 +24,6 @@ class PickingPass(RenderPass): 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")) @@ -47,7 +46,7 @@ class PickingPass(RenderPass): self.release() ## Get the distance in mm from the camera to at a certain pixel coordinate. - def getPickedDepth(self, x, y) -> float: + def getPickedDepth(self, x: int, y: int) -> float: output = self.getOutput() window_size = self._renderer.getWindowSize() @@ -56,14 +55,14 @@ class PickingPass(RenderPass): 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 None + 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, y) -> Vector: + def getPickedPosition(self, x: int, y: int) -> Vector: distance = self.getPickedDepth(x, y) ray = self._scene.getActiveCamera().getRay(x, y) From c25711797e6ccb9d7924ea7c724c5a40529dcf9f Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 15 Mar 2018 12:46:22 +0100 Subject: [PATCH 07/12] Click support eraser mesh to remove it from the scene --- plugins/SupportEraser/SupportEraser.py | 27 +++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 65d22bcdfd..bbb3fefca5 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -7,6 +7,7 @@ from UM.Application import Application from UM.Event import Event, MouseEvent from UM.Mesh.MeshBuilder import MeshBuilder from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation +from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation from UM.Settings.SettingInstance import SettingInstance from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator @@ -24,24 +25,39 @@ class SupportEraser(Tool): self._shortcut_key = Qt.Key_G self._controller = Application.getInstance().getController() + self._selection_pass = None Application.getInstance().globalContainerStackChanged.connect(self._updateEnabled) def event(self, event): super().event(event) if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(): - active_camera = self._controller.getScene().getActiveCamera() + + 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)) + + node_stack = picked_node.callDecoration("getStack") + if node_stack: + if node_stack.getProperty("anti_overhang_mesh", "value"): + self._removeEraserMesh(picked_node) + return + + elif node_stack.getProperty("support_mesh", "value") or node_stack.getProperty("infill_mesh", "value") or node_stack.getProperty("cutting_mesh", "value"): + 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_position) + self._createEraserMesh(picked_node, picked_position) - def _createEraserMesh(self, position: Vector): + def _createEraserMesh(self, parent: CuraSceneNode, position: Vector): node = CuraSceneNode() node.setName("Eraser") @@ -76,6 +92,11 @@ class SupportEraser(Tool): op.push() Application.getInstance().getController().getScene().sceneChanged.emit(node) + def _removeEraserMesh(self, node: CuraSceneNode): + op = RemoveSceneNodeOperation(node) + op.push() + Application.getInstance().getController().getScene().sceneChanged.emit(node) + def _updateEnabled(self): plugin_enabled = False From a0c44192da1f1455d7ba8b564d95676d369cdb17 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 15 Mar 2018 14:17:59 +0100 Subject: [PATCH 08/12] Add support eraser meshes to group so it does not drop --- plugins/SupportEraser/SupportEraser.py | 82 +++++++++++++++++--------- 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index bbb3fefca5..8f8e9deb52 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -1,24 +1,35 @@ # 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, MouseEvent -from UM.Mesh.MeshBuilder import MeshBuilder -from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation -from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation -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 UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator -from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator -from cura.PickingPass import PickingPass import os import os.path +from PyQt5.QtCore import Qt, QUrl + +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__() @@ -45,6 +56,7 @@ class SupportEraser(Tool): return 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 @@ -73,22 +85,36 @@ 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) From c3c096aaa015936441d8e7360c427f5af67c18aa Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 15 Mar 2018 14:55:15 +0100 Subject: [PATCH 09/12] Select the picked node so the group does not get drawn --- plugins/SupportEraser/SupportEraser.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 8f8e9deb52..d87a887d1b 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -118,6 +118,12 @@ class SupportEraser(Tool): 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 Selection.isSelected(group): + Selection.remove(group) + if not Selection.isSelected(parent): + Selection.add(parent) + def _removeEraserMesh(self, node: CuraSceneNode): op = RemoveSceneNodeOperation(node) op.push() From b9bf78d36ce67bbcd5163d479778b69b362b51ce Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 15 Mar 2018 15:05:56 +0100 Subject: [PATCH 10/12] Remove group when "parent" is the only node in the group --- plugins/SupportEraser/SupportEraser.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index d87a887d1b..ee59fc5258 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -125,10 +125,24 @@ class SupportEraser(Tool): Selection.add(parent) def _removeEraserMesh(self, node: CuraSceneNode): - op = RemoveSceneNodeOperation(node) + 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 Selection.isSelected(group): + Selection.remove(group) + if parent and not Selection.isSelected(parent): + Selection.add(parent) + def _updateEnabled(self): plugin_enabled = False From 2a811c62d8c2b3dcb07ab84378a33047a232c092 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 15 Mar 2018 23:58:22 +0100 Subject: [PATCH 11/12] Ignore the first press after the selection has been cleared if the selection is cleared with this tool active, there is no way to switch to another tool than to re-select an object (by clicking it) because the tool buttons in the toolbar will have been disabled. The mouse-event happens after the selection is changed, so we need to keep track of what the selection was previously by monitoring the selection state. --- plugins/SupportEraser/SupportEraser.py | 47 +++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index ee59fc5258..2afb3dfaab 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -4,7 +4,7 @@ import os import os.path -from PyQt5.QtCore import Qt, QUrl +from PyQt5.QtCore import Qt, QTimer from UM.Math.Vector import Vector from UM.Tool import Tool @@ -39,10 +39,28 @@ class SupportEraser(Tool): 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.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 if self._selection_pass is None: # The selection renderpass is used to identify objects in the current view @@ -119,10 +137,10 @@ class SupportEraser(Tool): Application.getInstance().getController().getScene().sceneChanged.emit(node) # Select the picked node so the group does not get drawn as a wireframe (yet) - if Selection.isSelected(group): - Selection.remove(group) if not Selection.isSelected(parent): Selection.add(parent) + if Selection.isSelected(group): + Selection.remove(group) def _removeEraserMesh(self, node: CuraSceneNode): group = node.getParent() @@ -138,10 +156,10 @@ class SupportEraser(Tool): Application.getInstance().getController().getScene().sceneChanged.emit(node) # Select the picked node so the group does not get drawn as a wireframe (yet) - if Selection.isSelected(group): - Selection.remove(group) if parent and not Selection.isSelected(parent): Selection.add(parent) + if Selection.isSelected(group): + Selection.remove(group) def _updateEnabled(self): plugin_enabled = False @@ -151,3 +169,22 @@ class SupportEraser(Tool): 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 From 8e26d27e805ad030a2623f89dcfd6f75451d5211 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Fri, 16 Mar 2018 13:15:24 +0100 Subject: [PATCH 12/12] Fix crash when clicking a non-slicable node --- plugins/SupportEraser/SupportEraser.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 2afb3dfaab..58624ea058 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -66,6 +66,9 @@ class SupportEraser(Tool): # 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 node_stack = picked_node.callDecoration("getStack") if node_stack: