diff --git a/plugins/XRayView/XRayPass.py b/cura/XRayPass.py similarity index 93% rename from plugins/XRayView/XRayPass.py rename to cura/XRayPass.py index a75d393b35..edc20ce62d 100644 --- a/plugins/XRayView/XRayPass.py +++ b/cura/XRayPass.py @@ -3,6 +3,7 @@ import os.path +from UM.Resources import Resources from UM.Application import Application from UM.PluginRegistry import PluginRegistry @@ -23,7 +24,7 @@ class XRayPass(RenderPass): def render(self): if not self._shader: - self._shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("XRayView"), "xray.shader")) + self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "xray.shader")) batch = RenderBatch(self._shader, type = RenderBatch.RenderType.NoType, backface_cull = False, blend_mode = RenderBatch.BlendMode.Additive) for node in DepthFirstIterator(self._scene.getRoot()): diff --git a/plugins/SolidView/SolidView.py b/plugins/SolidView/SolidView.py index 4f15bafedb..8c9967be03 100644 --- a/plugins/SolidView/SolidView.py +++ b/plugins/SolidView/SolidView.py @@ -1,22 +1,43 @@ -# Copyright (c) 2019 Ultimaker B.V. +# Copyright (c) 2020 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import os.path from UM.View.View import View from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Selection import Selection from UM.Resources import Resources +from PyQt5.QtGui import QOpenGLContext, QImage +from PyQt5.QtCore import QSize + +import numpy as np +import time + from UM.Application import Application -from UM.View.RenderBatch import RenderBatch +from UM.Logger import Logger +from UM.Message import Message from UM.Math.Color import Color +from UM.PluginRegistry import PluginRegistry +from UM.Platform import Platform +from UM.Event import Event + +from UM.View.RenderBatch import RenderBatch from UM.View.GL.OpenGL import OpenGL +from UM.i18n import i18nCatalog + from cura.Settings.ExtruderManager import ExtruderManager +from cura import XRayPass + import math +catalog = i18nCatalog("cura") + ## Standard view for mesh models. class SolidView(View): + _show_xray_warning_preference = "view/show_xray_warning" + def __init__(self): super().__init__() application = Application.getInstance() @@ -27,13 +48,31 @@ class SolidView(View): self._non_printing_shader = None self._support_mesh_shader = None + self._xray_shader = None + self._xray_pass = None + self._xray_composite_shader = None + self._composite_pass = None + self._extruders_model = None self._theme = None self._support_angle = 90 self._global_stack = None - Application.getInstance().engineCreatedSignal.connect(self._onGlobalContainerChanged) + self._old_composite_shader = None + self._old_layer_bindings = None + + self._next_xray_checking_time = time.time() + self._xray_checking_update_time = 1.0 # seconds + self._xray_warning_cooldown = 60 * 10 # reshow Model error message every 10 minutes + self._xray_warning_message = Message( + catalog.i18nc("@info:status", "Your model is not manifold. The highlighted areas indicate either missing or extraneous surfaces."), + lifetime = 60 * 5, # leave message for 5 minutes + title = catalog.i18nc("@info:title", "Model errors"), + ) + application.getPreferences().addPreference(self._show_xray_warning_preference, True) + + application.engineCreatedSignal.connect(self._onGlobalContainerChanged) def _onGlobalContainerChanged(self) -> None: if self._global_stack: @@ -92,6 +131,41 @@ class SolidView(View): self._support_mesh_shader.setUniformValue("u_vertical_stripes", True) self._support_mesh_shader.setUniformValue("u_width", 5.0) + if not Application.getInstance().getPreferences().getValue(self._show_xray_warning_preference): + self._xray_shader = None + self._xray_composite_shader = None + if self._composite_pass and 'xray' in self._composite_pass.getLayerBindings(): + self._composite_pass.setLayerBindings(self._old_layer_bindings) + self._composite_pass.setCompositeShader(self._old_composite_shader) + self._old_layer_bindings = None + self._old_composite_shader = None + self._xray_warning_message.hide() + else: + if not self._xray_shader: + self._xray_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "xray.shader")) + + if not self._xray_composite_shader: + self._xray_composite_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "xray_composite.shader")) + theme = Application.getInstance().getTheme() + self._xray_composite_shader.setUniformValue("u_background_color", Color(*theme.getColor("viewport_background").getRgb())) + self._xray_composite_shader.setUniformValue("u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb())) + + renderer = self.getRenderer() + if not self._composite_pass or not 'xray' in self._composite_pass.getLayerBindings(): + # Currently the RenderPass constructor requires a size > 0 + # This should be fixed in RenderPass's constructor. + self._xray_pass = XRayPass.XRayPass(1, 1) + + renderer.addRenderPass(self._xray_pass) + + if not self._composite_pass: + self._composite_pass = self.getRenderer().getRenderPass("composite") + + self._old_layer_bindings = self._composite_pass.getLayerBindings() + self._composite_pass.setLayerBindings(["default", "selection", "xray"]) + self._old_composite_shader = self._composite_pass.getCompositeShader() + self._composite_pass.setCompositeShader(self._xray_composite_shader) + def beginRendering(self): scene = self.getController().getScene() renderer = self.getRenderer() @@ -175,4 +249,65 @@ class SolidView(View): renderer.queueNode(scene.getRoot(), mesh = node.getBoundingBoxMesh(), mode = RenderBatch.RenderMode.LineLoop) def endRendering(self): - pass + # check whether the xray overlay is showing badness + if time.time() > self._next_xray_checking_time\ + and Application.getInstance().getPreferences().getValue(self._show_xray_warning_preference): + self._next_xray_checking_time = time.time() + self._xray_checking_update_time + + xray_img = self._xray_pass.getOutput() + xray_img = xray_img.convertToFormat(QImage.Format_RGB888) + + # We can't just read the image since the pixels are aligned to internal memory positions. + # xray_img.byteCount() != xray_img.width() * xray_img.height() * 3 + # The byte count is a little higher sometimes. We need to check the data per line, but fast using Numpy. + # See https://stackoverflow.com/questions/5810970/get-raw-data-from-qimage for a description of the problem. + # We can't use that solution though, since it doesn't perform well in Python. + class QImageArrayView: + """ + Class that ducktypes to be a Numpy ndarray. + """ + def __init__(self, qimage): + self.__array_interface__ = { + "shape": (qimage.height(), qimage.width()), + "typestr": "|u4", # Use 4 bytes per pixel rather than 3, since Numpy doesn't support 3. + "data": (int(qimage.bits()), False), + "strides": (qimage.bytesPerLine(), 3), # This does the magic: For each line, skip the correct number of bytes. Bytes per pixel is always 3 due to QImage.Format.Format_RGB888. + "version": 3 + } + array = np.asarray(QImageArrayView(xray_img)).view(np.dtype({ + "r": (np.uint8, 0, "red"), + "g": (np.uint8, 1, "green"), + "b": (np.uint8, 2, "blue"), + "a": (np.uint8, 3, "alpha") # Never filled since QImage was reformatted to RGB888. + }), np.recarray) + if np.any(np.mod(array.r, 2)): + self._next_xray_checking_time = time.time() + self._xray_warning_cooldown + self._xray_warning_message.show() + Logger.log("i", "X-Ray overlay found non-manifold pixels.") + + def event(self, event): + if event.type == Event.ViewActivateEvent: + # FIX: on Max OS X, somehow QOpenGLContext.currentContext() can become None during View switching. + # This can happen when you do the following steps: + # 1. Start Cura + # 2. Load a model + # 3. Switch to Custom mode + # 4. Select the model and click on the per-object tool icon + # 5. Switch view to Layer view or X-Ray + # 6. Cura will very likely crash + # It seems to be a timing issue that the currentContext can somehow be empty, but I have no clue why. + # This fix tries to reschedule the view changing event call on the Qt thread again if the current OpenGL + # context is None. + if Platform.isOSX(): + if QOpenGLContext.currentContext() is None: + Logger.log("d", "current context of OpenGL is empty on Mac OS X, will try to create shaders later") + Application.getInstance().callLater(lambda e = event: self.event(e)) + return + + + if event.type == Event.ViewDeactivateEvent: + if self._composite_pass and 'xray' in self._composite_pass.getLayerBindings(): + self.getRenderer().removeRenderPass(self._xray_pass) + self._composite_pass.setLayerBindings(self._old_layer_bindings) + self._composite_pass.setCompositeShader(self._old_composite_shader) + self._xray_warning_message.hide() diff --git a/plugins/SolidView/xray_overlay.shader b/plugins/SolidView/xray_overlay.shader new file mode 100755 index 0000000000..ba032b2123 --- /dev/null +++ b/plugins/SolidView/xray_overlay.shader @@ -0,0 +1,50 @@ +[shaders] +vertex = + uniform highp mat4 u_modelViewProjectionMatrix; + + attribute highp vec4 a_vertex; + + void main() + { + gl_Position = u_modelViewProjectionMatrix * a_vertex; + } + +fragment = + uniform lowp vec4 u_xray_error; + + void main() + { + gl_FragColor = u_xray_error; + } + +vertex41core = + #version 410 + uniform highp mat4 u_modelViewProjectionMatrix; + + in highp vec4 a_vertex; + + void main() + { + gl_Position = u_modelViewProjectionMatrix * a_vertex; + } + +fragment41core = + #version 410 + uniform lowp vec4 u_xray_error; + + out vec4 frag_color; + + void main() + { + + frag_color = u_xray_error; + } + +[defaults] +u_xray_error = [1.0, 1.0, 1.0, 1.0] + +[bindings] +u_modelViewProjectionMatrix = model_view_projection_matrix + +[attributes] +a_vertex = vertex diff --git a/plugins/XRayView/XRayView.py b/plugins/XRayView/XRayView.py index 88a5a441b8..8fc47cee74 100644 --- a/plugins/XRayView/XRayView.py +++ b/plugins/XRayView/XRayView.py @@ -1,13 +1,14 @@ -# Copyright (c) 2015 Ultimaker B.V. +# Copyright (c) 2020 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import os.path -from PyQt5.QtGui import QOpenGLContext +from PyQt5.QtGui import QOpenGLContext, QImage from UM.Application import Application from UM.Logger import Logger from UM.Math.Color import Color from UM.PluginRegistry import PluginRegistry +from UM.Resources import Resources from UM.Platform import Platform from UM.Event import Event from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator @@ -19,7 +20,8 @@ from cura.CuraApplication import CuraApplication from cura.CuraView import CuraView from cura.Scene.ConvexHullNode import ConvexHullNode -from . import XRayPass +from cura import XRayPass + ## View used to display a see-through version of objects with errors highlighted. class XRayView(CuraView): @@ -38,7 +40,7 @@ class XRayView(CuraView): renderer = self.getRenderer() if not self._xray_shader: - self._xray_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("XRayView"), "xray.shader")) + self._xray_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "xray.shader")) self._xray_shader.setUniformValue("u_color", Color(*Application.getInstance().getTheme().getColor("xray").getRgb())) for node in BreadthFirstIterator(scene.getRoot()): @@ -87,10 +89,8 @@ class XRayView(CuraView): self.getRenderer().addRenderPass(self._xray_pass) if not self._xray_composite_shader: - self._xray_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("XRayView"), "xray_composite.shader")) - theme = Application.getInstance().getTheme() + self._xray_composite_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "xray_composite.shader")) self._xray_composite_shader.setUniformValue("u_background_color", Color(*theme.getColor("viewport_background").getRgb())) - self._xray_composite_shader.setUniformValue("u_error_color", Color(*theme.getColor("xray_error").getRgb())) self._xray_composite_shader.setUniformValue("u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb())) if not self._composite_pass: diff --git a/resources/qml/Preferences/GeneralPage.qml b/resources/qml/Preferences/GeneralPage.qml index 4cd3afb4af..404c961a90 100644 --- a/resources/qml/Preferences/GeneralPage.qml +++ b/resources/qml/Preferences/GeneralPage.qml @@ -86,6 +86,8 @@ UM.PreferencesPage prefixJobNameCheckbox.checked = boolCheck(UM.Preferences.getValue("cura/jobname_prefix")) UM.Preferences.resetPreference("view/show_overhang"); showOverhangCheckbox.checked = boolCheck(UM.Preferences.getValue("view/show_overhang")) + UM.Preferences.resetPreference("view/show_xray_warning"); + showXrayErrorCheckbox.checked = boolCheck(UM.Preferences.getValue("view/show_warning")) UM.Preferences.resetPreference("view/center_on_select"); centerOnSelectCheckbox.checked = boolCheck(UM.Preferences.getValue("view/center_on_select")) UM.Preferences.resetPreference("view/invert_zoom"); @@ -337,6 +339,25 @@ UM.PreferencesPage } } + + UM.TooltipArea + { + width: childrenRect.width; + height: childrenRect.height; + + text: catalog.i18nc("@info:tooltip", "Highlight missing or extraneous surfaces of the model using warning signs. The toolpaths will often be missing parts of the intended geometry.") + + CheckBox + { + id: showXrayErrorCheckbox + + checked: boolCheck(UM.Preferences.getValue("view/show_xray_warning")) + onClicked: UM.Preferences.setValue("view/show_xray_warning", checked) + + text: catalog.i18nc("@option:check", "Display model errors"); + } + } + UM.TooltipArea { width: childrenRect.width; diff --git a/resources/shaders/overhang.shader b/resources/shaders/overhang.shader index 7f1b04dd59..b73ed3c701 100644 --- a/resources/shaders/overhang.shader +++ b/resources/shaders/overhang.shader @@ -38,9 +38,13 @@ fragment = varying highp vec3 f_vertex; varying highp vec3 f_normal; + float round(float f) + { + return sign(f) * floor(abs(f) + 0.5); + } + void main() { - mediump vec4 finalColor = vec4(0.0); // Ambient Component @@ -62,8 +66,10 @@ fragment = finalColor = (-normal.y > u_overhangAngle) ? u_overhangColor : finalColor; + vec3 grid = vec3(f_vertex.x - round(f_vertex.x), f_vertex.y - round(f_vertex.y), f_vertex.z - round(f_vertex.z)); + finalColor.a = dot(grid, grid) < 0.245 ? 0.667 : 1.0; + gl_FragColor = finalColor; - gl_FragColor.a = 1.0; } vertex41core = @@ -111,7 +117,6 @@ fragment41core = void main() { - mediump vec4 finalColor = vec4(0.0); // Ambient Component @@ -134,7 +139,9 @@ fragment41core = finalColor = (u_faceId != gl_PrimitiveID) ? ((-normal.y > u_overhangAngle) ? u_overhangColor : finalColor) : u_faceColor; frag_color = finalColor; - frag_color.a = 1.0; + + vec3 grid = f_vertex - round(f_vertex); + frag_color.a = dot(grid, grid) < 0.245 ? 0.667 : 1.0; } [defaults] diff --git a/plugins/XRayView/xray.shader b/resources/shaders/xray.shader similarity index 78% rename from plugins/XRayView/xray.shader rename to resources/shaders/xray.shader index 45cb16c44c..278b5b1dd3 100644 --- a/plugins/XRayView/xray.shader +++ b/resources/shaders/xray.shader @@ -12,7 +12,14 @@ vertex = } fragment = - uniform lowp vec4 u_color; + #ifdef GL_ES + #ifdef GL_FRAGMENT_PRECISION_HIGH + precision highp float; + #else + precision mediump float; + #endif // GL_FRAGMENT_PRECISION_HIGH + #endif // GL_ES + uniform vec4 u_color; void main() { @@ -34,7 +41,8 @@ vertex41core = fragment41core = #version 410 - uniform lowp vec4 u_color; + + uniform vec4 u_color; out vec4 frag_color; diff --git a/plugins/XRayView/xray_composite.shader b/resources/shaders/xray_composite.shader similarity index 59% rename from plugins/XRayView/xray_composite.shader rename to resources/shaders/xray_composite.shader index 7ea5287f96..c955d4fc18 100644 --- a/plugins/XRayView/xray_composite.shader +++ b/resources/shaders/xray_composite.shader @@ -28,8 +28,8 @@ fragment = uniform float u_outline_strength; uniform vec4 u_outline_color; - uniform vec4 u_error_color; uniform vec4 u_background_color; + uniform float u_xray_error_strength; const vec3 x_axis = vec3(1.0, 0.0, 0.0); const vec3 y_axis = vec3(0.0, 1.0, 0.0); @@ -39,6 +39,20 @@ fragment = float kernel[9]; + vec3 shiftHue(vec3 color, float hue) + { + // Make sure colors are distinct when grey-scale is used too: + if ((max(max(color.r, color.g), color.b) - min(min(color.r, color.g), color.b)) < 0.1) + { + color = vec3(1.0, 0.0, 0.0); + } + + // The actual hue shift: + const vec3 k = vec3(0.57735, 0.57735, 0.57735); + float cosAngle = cos(hue); + return vec3(color * cosAngle + cross(k, color) * sin(hue) + k * dot(k, color) * (1.0 - cosAngle)); + } + void main() { kernel[0] = 0.0; kernel[1] = 1.0; kernel[2] = 0.0; @@ -48,12 +62,18 @@ fragment = vec4 result = u_background_color; vec4 layer0 = texture2D(u_layer0, v_uvs); - result = layer0 * layer0.a + result * (1.0 - layer0.a); - - float intersection_count = (texture2D(u_layer2, v_uvs).r * 255.0) / 5.0; - if(mod(intersection_count, 2.0) == 1.0) + float hue_shift = (layer0.a - 0.333) * 6.2831853; + if (layer0.a > 0.5) { - result = u_error_color; + layer0.a = 1.0; + } + result = mix(result, layer0, layer0.a); + + float intersection_count = texture2D(u_layer2, v_uvs).r * 51.0; // (1 / .02) + 1 (+1 magically fixes issues with high intersection count models) + float rest = mod(intersection_count + .01, 2.0); + if (rest > 1.0 && rest < 1.5 && intersection_count < 49.0) + { + result = vec4(shiftHue(layer0.rgb, hue_shift), result.a); } vec4 sum = vec4(0.0); @@ -70,8 +90,10 @@ fragment = } else { - gl_FragColor = mix(result, vec4(abs(sum.a)) * u_outline_color, abs(sum.a)); + gl_FragColor = mix(result, u_outline_color, abs(sum.a)); } + + gl_FragColor.a = gl_FragColor.a > 0.5 ? 1.0 : 0.0; } vertex41core = @@ -98,8 +120,8 @@ fragment41core = uniform float u_outline_strength; uniform vec4 u_outline_color; - uniform vec4 u_error_color; uniform vec4 u_background_color; + uniform float u_xray_error_strength; const vec3 x_axis = vec3(1.0, 0.0, 0.0); const vec3 y_axis = vec3(0.0, 1.0, 0.0); @@ -110,6 +132,20 @@ fragment41core = float kernel[9]; + vec3 shiftHue(vec3 color, float hue) + { + // Make sure colors are distinct when grey-scale is used too: + if ((max(max(color.r, color.g), color.b) - min(min(color.r, color.g), color.b)) < 0.1) + { + color = vec3(1.0, 0.0, 0.0); + } + + // The actual hue shift: + const vec3 k = vec3(0.57735, 0.57735, 0.57735); + float cosAngle = cos(hue); + return vec3(color * cosAngle + cross(k, color) * sin(hue) + k * dot(k, color) * (1.0 - cosAngle)); + } + void main() { kernel[0] = 0.0; kernel[1] = 1.0; kernel[2] = 0.0; @@ -119,12 +155,18 @@ fragment41core = vec4 result = u_background_color; vec4 layer0 = texture(u_layer0, v_uvs); - result = layer0 * layer0.a + result * (1.0 - layer0.a); - - float intersection_count = (texture(u_layer2, v_uvs).r * 255.0) / 5.0; - if(mod(intersection_count, 2.0) == 1.0) + float hue_shift = (layer0.a - 0.333) * 6.2831853; + if (layer0.a > 0.5) { - result = u_error_color; + layer0.a = 1.0; + } + result = mix(result, layer0, layer0.a); + + float intersection_count = texture(u_layer2, v_uvs).r * 51; // (1 / .02) + 1 (+1 magically fixes issues with high intersection count models) + float rest = mod(intersection_count + .01, 2.0); + if (rest > 1.0 && rest < 1.5 && intersection_count < 49) + { + result = vec4(shiftHue(layer0.rgb, hue_shift), result.a); } vec4 sum = vec4(0.0); @@ -141,8 +183,10 @@ fragment41core = } else { - frag_color = mix(result, vec4(abs(sum.a)) * u_outline_color, abs(sum.a)); + frag_color = mix(result, u_outline_color, abs(sum.a)); } + + frag_color.a = frag_color.a > 0.5 ? 1.0 : 0.0; } [defaults] @@ -152,7 +196,6 @@ u_layer2 = 2 u_background_color = [0.965, 0.965, 0.965, 1.0] u_outline_strength = 1.0 u_outline_color = [0.05, 0.66, 0.89, 1.0] -u_error_color = [1.0, 0.0, 0.0, 1.0] [bindings] diff --git a/resources/themes/cura-dark-colorblind/theme.json b/resources/themes/cura-dark-colorblind/theme.json index 9559101d24..c98fb0c815 100644 --- a/resources/themes/cura-dark-colorblind/theme.json +++ b/resources/themes/cura-dark-colorblind/theme.json @@ -12,6 +12,8 @@ "model_overhang": [200, 0, 255, 255], + "xray_error_dark": [255, 0, 0, 255], + "xray_error_light": [255, 255, 0, 255], "xray": [26, 26, 62, 255], "xray_error": [255, 0, 0, 255], diff --git a/resources/themes/cura-light-colorblind/theme.json b/resources/themes/cura-light-colorblind/theme.json index 10349acbfd..5628fad880 100644 --- a/resources/themes/cura-light-colorblind/theme.json +++ b/resources/themes/cura-light-colorblind/theme.json @@ -10,9 +10,10 @@ "y_axis": [64, 64, 255, 255], "model_default": [156, 201, 36, 255], "model_overhang": [200, 0, 255, 255], - "model_selection_outline": [12, 169, 227, 255], + "xray_error_dark": [255, 0, 0, 255], + "xray_error_light": [255, 255, 0, 255], "xray": [26, 26, 62, 255], "xray_error": [255, 0, 0, 255], diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index dcb2350075..1640395c0b 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -369,6 +369,8 @@ "model_selection_outline": [50, 130, 255, 255], "model_non_printing": [122, 122, 122, 255], + "xray_error_dark": [255, 0, 0, 255], + "xray_error_light": [255, 255, 0, 255], "xray": [26, 26, 62, 255], "xray_error": [255, 0, 0, 255],