Browse Source

CURA-4526 Add Simulation View plugin

Diego Prado Gesto 7 years ago
parent
commit
2df06bbbb9

+ 325 - 0
plugins/SimulationView/LayerSlider.qml

@@ -0,0 +1,325 @@
+// Copyright (c) 2017 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.2
+import QtQuick.Controls 1.2
+import QtQuick.Layouts 1.1
+import QtQuick.Controls.Styles 1.1
+
+import UM 1.0 as UM
+import Cura 1.0 as Cura
+
+Item {
+    id: sliderRoot
+
+    // handle properties
+    property real handleSize: 10
+    property real handleRadius: handleSize / 2
+    property real minimumRangeHandleSize: handleSize / 2
+    property color upperHandleColor: "black"
+    property color lowerHandleColor: "black"
+    property color rangeHandleColor: "black"
+    property color handleActiveColor: "white"
+    property real handleLabelWidth: width
+    property var activeHandle: upperHandle
+
+    // track properties
+    property real trackThickness: 4 // width of the slider track
+    property real trackRadius: trackThickness / 2
+    property color trackColor: "white"
+    property real trackBorderWidth: 1 // width of the slider track border
+    property color trackBorderColor: "black"
+
+    // value properties
+    property real maximumValue: 100
+    property real minimumValue: 0
+    property real minimumRange: 0 // minimum range allowed between min and max values
+    property bool roundValues: true
+    property real upperValue: maximumValue
+    property real lowerValue: minimumValue
+
+    property bool layersVisible: true
+
+    function getUpperValueFromSliderHandle () {
+        return upperHandle.getValue()
+    }
+
+    function setUpperValue (value) {
+        upperHandle.setValue(value)
+        updateRangeHandle()
+    }
+
+    function getLowerValueFromSliderHandle () {
+        return lowerHandle.getValue()
+    }
+
+    function setLowerValue (value) {
+        lowerHandle.setValue(value)
+        updateRangeHandle()
+    }
+
+    function updateRangeHandle () {
+        rangeHandle.height = lowerHandle.y - (upperHandle.y + upperHandle.height)
+    }
+
+    // set the active handle to show only one label at a time
+    function setActiveHandle (handle) {
+        activeHandle = handle
+    }
+
+    // slider track
+    Rectangle {
+        id: track
+
+        width: sliderRoot.trackThickness
+        height: sliderRoot.height - sliderRoot.handleSize
+        radius: sliderRoot.trackRadius
+        anchors.centerIn: sliderRoot
+        color: sliderRoot.trackColor
+        border.width: sliderRoot.trackBorderWidth
+        border.color: sliderRoot.trackBorderColor
+        visible: sliderRoot.layersVisible
+    }
+
+    // Range handle
+    Item {
+        id: rangeHandle
+
+        y: upperHandle.y + upperHandle.height
+        width: sliderRoot.handleSize
+        height: sliderRoot.minimumRangeHandleSize
+        anchors.horizontalCenter: sliderRoot.horizontalCenter
+        visible: sliderRoot.layersVisible
+
+        // set the new value when dragging
+        function onHandleDragged () {
+
+            upperHandle.y = y - upperHandle.height
+            lowerHandle.y = y + height
+
+            var upperValue = sliderRoot.getUpperValueFromSliderHandle()
+            var lowerValue = sliderRoot.getLowerValueFromSliderHandle()
+
+            // set both values after moving the handle position
+            UM.SimulationView.setCurrentLayer(upperValue)
+            UM.SimulationView.setMinimumLayer(lowerValue)
+        }
+
+        function setValue (value) {
+            var range = sliderRoot.upperValue - sliderRoot.lowerValue
+            value = Math.min(value, sliderRoot.maximumValue)
+            value = Math.max(value, sliderRoot.minimumValue + range)
+
+            UM.SimulationView.setCurrentLayer(value)
+            UM.SimulationView.setMinimumLayer(value - range)
+        }
+
+        Rectangle {
+            width: sliderRoot.trackThickness - 2 * sliderRoot.trackBorderWidth
+            height: parent.height + sliderRoot.handleSize
+            anchors.centerIn: parent
+            color: sliderRoot.rangeHandleColor
+        }
+
+        MouseArea {
+            anchors.fill: parent
+
+            drag {
+                target: parent
+                axis: Drag.YAxis
+                minimumY: upperHandle.height
+                maximumY: sliderRoot.height - (rangeHandle.height + lowerHandle.height)
+            }
+
+            onPositionChanged: parent.onHandleDragged()
+            onPressed: sliderRoot.setActiveHandle(rangeHandle)
+        }
+
+        SimulationSliderLabel {
+            id: rangleHandleLabel
+
+            height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
+            x: parent.x - width - UM.Theme.getSize("default_margin").width
+            anchors.verticalCenter: parent.verticalCenter
+            target: Qt.point(sliderRoot.width, y + height / 2)
+            visible: sliderRoot.activeHandle == parent
+
+            // custom properties
+            maximumValue: sliderRoot.maximumValue
+            value: sliderRoot.upperValue
+            busy: UM.SimulationView.busy
+            setValue: rangeHandle.setValue // connect callback functions
+        }
+    }
+
+    // Upper handle
+    Rectangle {
+        id: upperHandle
+
+        y: sliderRoot.height - (sliderRoot.minimumRangeHandleSize + 2 * sliderRoot.handleSize)
+        width: sliderRoot.handleSize
+        height: sliderRoot.handleSize
+        anchors.horizontalCenter: sliderRoot.horizontalCenter
+        radius: sliderRoot.handleRadius
+        color: upperHandleLabel.activeFocus ? sliderRoot.handleActiveColor : sliderRoot.upperHandleColor
+        visible: sliderRoot.layersVisible
+
+        function onHandleDragged () {
+
+            // don't allow the lower handle to be heigher than the upper handle
+            if (lowerHandle.y - (y + height) < sliderRoot.minimumRangeHandleSize) {
+                lowerHandle.y = y + height + sliderRoot.minimumRangeHandleSize
+            }
+
+            // update the range handle
+            sliderRoot.updateRangeHandle()
+
+            // set the new value after moving the handle position
+            UM.SimulationView.setCurrentLayer(getValue())
+        }
+
+        // get the upper value based on the slider position
+        function getValue () {
+            var result = y / (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize))
+            result = sliderRoot.maximumValue + result * (sliderRoot.minimumValue - (sliderRoot.maximumValue - sliderRoot.minimumValue))
+            result = sliderRoot.roundValues ? Math.round(result) : result
+            return result
+        }
+
+        // set the slider position based on the upper value
+        function setValue (value) {
+
+            UM.SimulationView.setCurrentLayer(value)
+
+            var diff = (value - sliderRoot.maximumValue) / (sliderRoot.minimumValue - sliderRoot.maximumValue)
+            var newUpperYPosition = Math.round(diff * (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)))
+            y = newUpperYPosition
+
+            // update the range handle
+            sliderRoot.updateRangeHandle()
+        }
+
+        Keys.onUpPressed: upperHandleLabel.setValue(upperHandleLabel.value + ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
+        Keys.onDownPressed: upperHandleLabel.setValue(upperHandleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
+
+        // dragging
+        MouseArea {
+            anchors.fill: parent
+
+            drag {
+                target: parent
+                axis: Drag.YAxis
+                minimumY: 0
+                maximumY: sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)
+            }
+
+            onPositionChanged: parent.onHandleDragged()
+            onPressed: {
+                sliderRoot.setActiveHandle(upperHandle)
+                upperHandleLabel.forceActiveFocus()
+            }
+        }
+
+        SimulationSliderLabel {
+            id: upperHandleLabel
+
+            height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
+            x: parent.x - width - UM.Theme.getSize("default_margin").width
+            anchors.verticalCenter: parent.verticalCenter
+            target: Qt.point(sliderRoot.width, y + height / 2)
+            visible: sliderRoot.activeHandle == parent
+
+            // custom properties
+            maximumValue: sliderRoot.maximumValue
+            value: sliderRoot.upperValue
+            busy: UM.SimulationView.busy
+            setValue: upperHandle.setValue // connect callback functions
+        }
+    }
+
+    // Lower handle
+    Rectangle {
+        id: lowerHandle
+
+        y: sliderRoot.height - sliderRoot.handleSize
+        width: parent.handleSize
+        height: parent.handleSize
+        anchors.horizontalCenter: parent.horizontalCenter
+        radius: sliderRoot.handleRadius
+        color: lowerHandleLabel.activeFocus ? sliderRoot.handleActiveColor : sliderRoot.lowerHandleColor
+
+        visible: sliderRoot.layersVisible
+
+        function onHandleDragged () {
+
+            // don't allow the upper handle to be lower than the lower handle
+            if (y - (upperHandle.y + upperHandle.height) < sliderRoot.minimumRangeHandleSize) {
+                upperHandle.y = y - (upperHandle.heigth + sliderRoot.minimumRangeHandleSize)
+            }
+
+            // update the range handle
+            sliderRoot.updateRangeHandle()
+
+            // set the new value after moving the handle position
+            UM.SimulationView.setMinimumLayer(getValue())
+        }
+
+        // get the lower value from the current slider position
+        function getValue () {
+            var result = (y - (sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)) / (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize));
+            result = sliderRoot.maximumValue - sliderRoot.minimumRange + result * (sliderRoot.minimumValue - (sliderRoot.maximumValue - sliderRoot.minimumRange))
+            result = sliderRoot.roundValues ? Math.round(result) : result
+            return result
+        }
+
+        // set the slider position based on the lower value
+        function setValue (value) {
+
+            UM.SimulationView.setMinimumLayer(value)
+
+            var diff = (value - sliderRoot.maximumValue) / (sliderRoot.minimumValue - sliderRoot.maximumValue)
+            var newLowerYPosition = Math.round((sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize) + diff * (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)))
+            y = newLowerYPosition
+
+            // update the range handle
+            sliderRoot.updateRangeHandle()
+        }
+
+        Keys.onUpPressed: lowerHandleLabel.setValue(lowerHandleLabel.value + ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
+        Keys.onDownPressed: lowerHandleLabel.setValue(lowerHandleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
+
+        // dragging
+        MouseArea {
+            anchors.fill: parent
+
+            drag {
+                target: parent
+                axis: Drag.YAxis
+                minimumY: upperHandle.height + sliderRoot.minimumRangeHandleSize
+                maximumY: sliderRoot.height - parent.height
+            }
+
+            onPositionChanged: parent.onHandleDragged()
+            onPressed: {
+                sliderRoot.setActiveHandle(lowerHandle)
+                lowerHandleLabel.forceActiveFocus()
+            }
+        }
+
+        SimulationSliderLabel {
+            id: lowerHandleLabel
+
+            height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
+            x: parent.x - width - UM.Theme.getSize("default_margin").width
+            anchors.verticalCenter: parent.verticalCenter
+            target: Qt.point(sliderRoot.width, y + height / 2)
+            visible: sliderRoot.activeHandle == parent
+
+            // custom properties
+            maximumValue: sliderRoot.maximumValue
+            value: sliderRoot.lowerValue
+            busy: UM.SimulationView.busy
+            setValue: lowerHandle.setValue // connect callback functions
+        }
+    }
+}

+ 49 - 0
plugins/SimulationView/NozzleNode.py

@@ -0,0 +1,49 @@
+# Copyright (c) 2017 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.PluginRegistry import PluginRegistry
+from UM.Scene.SceneNode import SceneNode
+from UM.View.GL.OpenGL import OpenGL
+from UM.Resources import Resources
+
+import os
+
+class NozzleNode(SceneNode):
+    def __init__(self, parent = None):
+        super().__init__(parent)
+
+        self._shader = None
+        self.setCalculateBoundingBox(False)
+        self._createNozzleMesh()
+
+    def _createNozzleMesh(self):
+        mesh_file = "resources/nozzle.stl"
+        try:
+            path = os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), mesh_file)
+        except FileNotFoundError:
+            path = ""
+
+        reader = Application.getInstance().getMeshFileHandler().getReaderForFile(path)
+        node = reader.read(path)
+
+        if node.getMeshData():
+            self.setMeshData(node.getMeshData())
+
+    def render(self, renderer):
+        # Avoid to render if it is not visible
+        if not self.isVisible():
+            return False
+
+        if not self._shader:
+            # We now misuse the platform shader, as it actually supports textures
+            self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader"))
+            self._shader.setUniformValue("u_color", Color(*Application.getInstance().getTheme().getColor("layerview_nozzle").getRgb()))
+            # Set the opacity to 0, so that the template is in full control.
+            self._shader.setUniformValue("u_opacity", 0)
+
+        if self.getMeshData():
+            renderer.queueNode(self, shader = self._shader, transparent = True)
+            return True

+ 161 - 0
plugins/SimulationView/PathSlider.qml

@@ -0,0 +1,161 @@
+// Copyright (c) 2017 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.2
+import QtQuick.Controls 1.2
+import QtQuick.Layouts 1.1
+import QtQuick.Controls.Styles 1.1
+
+import UM 1.0 as UM
+import Cura 1.0 as Cura
+
+Item {
+    id: sliderRoot
+
+    // handle properties
+    property real handleSize: 10
+    property real handleRadius: handleSize / 2
+    property color handleColor: "black"
+    property color handleActiveColor: "white"
+    property color rangeColor: "black"
+    property real handleLabelWidth: width
+
+    // track properties
+    property real trackThickness: 4 // width of the slider track
+    property real trackRadius: trackThickness / 2
+    property color trackColor: "white"
+    property real trackBorderWidth: 1 // width of the slider track border
+    property color trackBorderColor: "black"
+
+    // value properties
+    property real maximumValue: 100
+    property bool roundValues: true
+    property real handleValue: maximumValue
+
+    property bool pathsVisible: true
+
+    function getHandleValueFromSliderHandle () {
+        return handle.getValue()
+    }
+
+    function setHandleValue (value) {
+        handle.setValue(value)
+        updateRangeHandle()
+    }
+
+    function updateRangeHandle () {
+        rangeHandle.width = handle.x - sliderRoot.handleSize
+    }
+
+    // slider track
+    Rectangle {
+        id: track
+
+        width: sliderRoot.width - sliderRoot.handleSize
+        height: sliderRoot.trackThickness
+        radius: sliderRoot.trackRadius
+        anchors.centerIn: sliderRoot
+        color: sliderRoot.trackColor
+        border.width: sliderRoot.trackBorderWidth
+        border.color: sliderRoot.trackBorderColor
+        visible: sliderRoot.pathsVisible
+    }
+
+    // Progress indicator
+    Item {
+        id: rangeHandle
+
+        x: handle.width
+        height: sliderRoot.handleSize
+        width: handle.x - sliderRoot.handleSize
+        anchors.verticalCenter: sliderRoot.verticalCenter
+        visible: sliderRoot.pathsVisible
+
+        Rectangle {
+            height: sliderRoot.trackThickness - 2 * sliderRoot.trackBorderWidth
+            width: parent.width + sliderRoot.handleSize
+            anchors.centerIn: parent
+            color: sliderRoot.rangeColor
+        }
+    }
+
+    // Handle
+    Rectangle {
+        id: handle
+
+        x: sliderRoot.handleSize
+        width: sliderRoot.handleSize
+        height: sliderRoot.handleSize
+        anchors.verticalCenter: sliderRoot.verticalCenter
+        radius: sliderRoot.handleRadius
+        color: handleLabel.activeFocus ? sliderRoot.handleActiveColor : sliderRoot.handleColor
+        visible: sliderRoot.pathsVisible
+
+        function onHandleDragged () {
+
+            // update the range handle
+            sliderRoot.updateRangeHandle()
+
+            // set the new value after moving the handle position
+            UM.SimulationView.setCurrentPath(getValue())
+        }
+
+        // get the value based on the slider position
+        function getValue () {
+            var result = x / (sliderRoot.width - sliderRoot.handleSize)
+            result = result * sliderRoot.maximumValue
+            result = sliderRoot.roundValues ? Math.round(result) : result
+            return result
+        }
+
+        // set the slider position based on the value
+        function setValue (value) {
+
+            UM.SimulationView.setCurrentPath(value)
+
+            var diff = value / sliderRoot.maximumValue
+            var newXPosition = Math.round(diff * (sliderRoot.width - sliderRoot.handleSize))
+            x = newXPosition
+
+            // update the range handle
+            sliderRoot.updateRangeHandle()
+        }
+
+        Keys.onRightPressed: handleLabel.setValue(handleLabel.value + ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
+        Keys.onLeftPressed: handleLabel.setValue(handleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
+
+        // dragging
+        MouseArea {
+            anchors.fill: parent
+
+            drag {
+                target: parent
+                axis: Drag.XAxis
+                minimumX: 0
+                maximumX: sliderRoot.width - sliderRoot.handleSize
+            }
+            onPressed: {
+                handleLabel.forceActiveFocus()
+            }
+
+            onPositionChanged: parent.onHandleDragged()
+        }
+
+        SimulationSliderLabel {
+            id: handleLabel
+
+            height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
+            y: parent.y + sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
+            anchors.horizontalCenter: parent.horizontalCenter
+            target: Qt.point(x + width / 2, sliderRoot.height)
+            visible: false
+            startFrom: 0
+
+            // custom properties
+            maximumValue: sliderRoot.maximumValue
+            value: sliderRoot.handleValue
+            busy: UM.SimulationView.busy
+            setValue: handle.setValue // connect callback functions
+        }
+    }
+}

+ 186 - 0
plugins/SimulationView/SimulationPass.py

@@ -0,0 +1,186 @@
+# Copyright (c) 2017 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from UM.Math.Color import Color
+from UM.Math.Vector import Vector
+from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
+from UM.Resources import Resources
+from UM.Scene.SceneNode import SceneNode
+from UM.Scene.ToolHandle import ToolHandle
+from UM.Application import Application
+from UM.PluginRegistry import PluginRegistry
+
+from UM.View.RenderPass import RenderPass
+from UM.View.RenderBatch import RenderBatch
+from UM.View.GL.OpenGL import OpenGL
+
+from cura.Settings.ExtruderManager import ExtruderManager
+
+
+import os.path
+
+## RenderPass used to display g-code paths.
+from .NozzleNode import NozzleNode
+
+
+class SimulationPass(RenderPass):
+    def __init__(self, width, height):
+        super().__init__("simulationview", width, height)
+
+        self._layer_shader = None
+        self._layer_shadow_shader = None
+        self._current_shader = None # This shader will be the shadow or the normal depending if the user wants to see the paths or the layers
+        self._tool_handle_shader = None
+        self._nozzle_shader = None
+        self._old_current_layer = 0
+        self._old_current_path = 0
+        self._gl = OpenGL.getInstance().getBindingsObject()
+        self._scene = Application.getInstance().getController().getScene()
+        self._extruder_manager = ExtruderManager.getInstance()
+
+        self._layer_view = None
+        self._compatibility_mode = None
+
+    def setSimulationView(self, layerview):
+        self._layer_view = layerview
+        self._compatibility_mode = layerview.getCompatibilityMode()
+
+    def render(self):
+        if not self._layer_shader:
+            if self._compatibility_mode:
+                shader_filename = "layers.shader"
+                shadow_shader_filename = "layers_shadow.shader"
+            else:
+                shader_filename = "layers3d.shader"
+                shadow_shader_filename = "layers3d_shadow.shader"
+            self._layer_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), shader_filename))
+            self._layer_shadow_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), shadow_shader_filename))
+            self._current_shader = self._layer_shader
+        # Use extruder 0 if the extruder manager reports extruder index -1 (for single extrusion printers)
+        self._layer_shader.setUniformValue("u_active_extruder", float(max(0, self._extruder_manager.activeExtruderIndex)))
+        if self._layer_view:
+            self._layer_shader.setUniformValue("u_max_feedrate", self._layer_view.getMaxFeedrate())
+            self._layer_shader.setUniformValue("u_min_feedrate", self._layer_view.getMinFeedrate())
+            self._layer_shader.setUniformValue("u_max_thickness", self._layer_view.getMaxThickness())
+            self._layer_shader.setUniformValue("u_min_thickness", self._layer_view.getMinThickness())
+            self._layer_shader.setUniformValue("u_layer_view_type", self._layer_view.getSimulationViewType())
+            self._layer_shader.setUniformValue("u_extruder_opacity", self._layer_view.getExtruderOpacities())
+            self._layer_shader.setUniformValue("u_show_travel_moves", self._layer_view.getShowTravelMoves())
+            self._layer_shader.setUniformValue("u_show_helpers", self._layer_view.getShowHelpers())
+            self._layer_shader.setUniformValue("u_show_skin", self._layer_view.getShowSkin())
+            self._layer_shader.setUniformValue("u_show_infill", self._layer_view.getShowInfill())
+        else:
+            #defaults
+            self._layer_shader.setUniformValue("u_max_feedrate", 1)
+            self._layer_shader.setUniformValue("u_min_feedrate", 0)
+            self._layer_shader.setUniformValue("u_max_thickness", 1)
+            self._layer_shader.setUniformValue("u_min_thickness", 0)
+            self._layer_shader.setUniformValue("u_layer_view_type", 1)
+            self._layer_shader.setUniformValue("u_extruder_opacity", [1, 1, 1, 1])
+            self._layer_shader.setUniformValue("u_show_travel_moves", 0)
+            self._layer_shader.setUniformValue("u_show_helpers", 1)
+            self._layer_shader.setUniformValue("u_show_skin", 1)
+            self._layer_shader.setUniformValue("u_show_infill", 1)
+
+        if not self._tool_handle_shader:
+            self._tool_handle_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "toolhandle.shader"))
+
+        if not self._nozzle_shader:
+            self._nozzle_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader"))
+            self._nozzle_shader.setUniformValue("u_color", Color(*Application.getInstance().getTheme().getColor("layerview_nozzle").getRgb()))
+
+        self.bind()
+
+        tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Solid)
+        head_position = None  # Indicates the current position of the print head
+        nozzle_node = None
+
+        for node in DepthFirstIterator(self._scene.getRoot()):
+
+            if isinstance(node, ToolHandle):
+                tool_handle_batch.addItem(node.getWorldTransformation(), mesh = node.getSolidMesh())
+
+            elif isinstance(node, NozzleNode):
+                nozzle_node = node
+                nozzle_node.setVisible(False)
+
+            elif isinstance(node, SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible():
+                layer_data = node.callDecoration("getLayerData")
+                if not layer_data:
+                    continue
+
+                # Render all layers below a certain number as line mesh instead of vertices.
+                if self._layer_view._current_layer_num > -1 and ((not self._layer_view._only_show_top_layers) or (not self._layer_view.getCompatibilityMode())):
+                    start = 0
+                    end = 0
+                    element_counts = layer_data.getElementCounts()
+                    for layer in sorted(element_counts.keys()):
+                        # In the current layer, we show just the indicated paths
+                        if layer == self._layer_view._current_layer_num:
+                            # We look for the position of the head, searching the point of the current path
+                            index = self._layer_view._current_path_num
+                            offset = 0
+                            for polygon in layer_data.getLayer(layer).polygons:
+                                # The size indicates all values in the two-dimension array, and the second dimension is
+                                # always size 3 because we have 3D points.
+                                if index >= polygon.data.size // 3 - offset:
+                                    index -= polygon.data.size // 3 - offset
+                                    offset = 1  # This is to avoid the first point when there is more than one polygon, since has the same value as the last point in the previous polygon
+                                    continue
+                                # The head position is calculated and translated
+                                head_position = Vector(polygon.data[index+offset][0], polygon.data[index+offset][1], polygon.data[index+offset][2]) + node.getWorldPosition()
+                                break
+                            break
+                        if self._layer_view._minimum_layer_num > layer:
+                            start += element_counts[layer]
+                        end += element_counts[layer]
+
+                    # Calculate the range of paths in the last layer
+                    current_layer_start = end
+                    current_layer_end = end + self._layer_view._current_path_num * 2 # Because each point is used twice
+
+                    # This uses glDrawRangeElements internally to only draw a certain range of lines.
+                    # All the layers but the current selected layer are rendered first
+                    if self._old_current_path != self._layer_view._current_path_num:
+                        self._current_shader = self._layer_shadow_shader
+                    if not self._layer_view.isSimulationRunning() and self._old_current_layer != self._layer_view._current_layer_num:
+                        self._current_shader = self._layer_shader
+
+                    layers_batch = RenderBatch(self._current_shader, type = RenderBatch.RenderType.Solid, mode = RenderBatch.RenderMode.Lines, range = (start, end))
+                    layers_batch.addItem(node.getWorldTransformation(), layer_data)
+                    layers_batch.render(self._scene.getActiveCamera())
+
+                    # Current selected layer is rendered
+                    current_layer_batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid, mode = RenderBatch.RenderMode.Lines, range = (current_layer_start, current_layer_end))
+                    current_layer_batch.addItem(node.getWorldTransformation(), layer_data)
+                    current_layer_batch.render(self._scene.getActiveCamera())
+
+                    self._old_current_layer = self._layer_view._current_layer_num
+                    self._old_current_path = self._layer_view._current_path_num
+
+                # Create a new batch that is not range-limited
+                batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid)
+
+                if self._layer_view.getCurrentLayerMesh():
+                    batch.addItem(node.getWorldTransformation(), self._layer_view.getCurrentLayerMesh())
+
+                if self._layer_view.getCurrentLayerJumps():
+                    batch.addItem(node.getWorldTransformation(), self._layer_view.getCurrentLayerJumps())
+
+                if len(batch.items) > 0:
+                    batch.render(self._scene.getActiveCamera())
+
+        # The nozzle is drawn once we know the correct position
+        if self._layer_view.getActivity() and nozzle_node is not None:
+            if head_position is not None:
+                nozzle_node.setVisible(True)
+                nozzle_node.setPosition(head_position)
+                nozzle_batch = RenderBatch(self._nozzle_shader, type = RenderBatch.RenderType.Solid)
+                nozzle_batch.addItem(nozzle_node.getWorldTransformation(), mesh = nozzle_node.getMeshData())
+                nozzle_batch.render(self._scene.getActiveCamera())
+
+        # Render toolhandles on top of the layerview
+        if len(tool_handle_batch.items) > 0:
+            tool_handle_batch.render(self._scene.getActiveCamera())
+
+        self.release()

+ 104 - 0
plugins/SimulationView/SimulationSliderLabel.qml

@@ -0,0 +1,104 @@
+// Copyright (c) 2017 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.2
+import QtQuick.Controls 1.2
+import QtQuick.Layouts 1.1
+import QtQuick.Controls.Styles 1.1
+
+import UM 1.0 as UM
+import Cura 1.0 as Cura
+
+UM.PointingRectangle {
+    id: sliderLabelRoot
+
+    // custom properties
+    property real maximumValue: 100
+    property real value: 0
+    property var setValue // Function
+    property bool busy: false
+    property int startFrom: 1
+
+    target: Qt.point(parent.width, y + height / 2)
+    arrowSize: UM.Theme.getSize("default_arrow").width
+    height: parent.height
+    width: valueLabel.width + UM.Theme.getSize("default_margin").width
+    visible: false
+
+    // make sure the text field is focussed when pressing the parent handle
+    // needed to connect the key bindings when switching active handle
+    onVisibleChanged: if (visible) valueLabel.forceActiveFocus()
+
+    color: UM.Theme.getColor("tool_panel_background")
+    borderColor: UM.Theme.getColor("lining")
+    borderWidth: UM.Theme.getSize("default_lining").width
+
+    Behavior on height {
+        NumberAnimation {
+            duration: 50
+        }
+    }
+
+    // catch all mouse events so they're not handled by underlying 3D scene
+    MouseArea {
+        anchors.fill: parent
+    }
+
+    TextField {
+        id: valueLabel
+
+        anchors {
+            left: parent.left
+            leftMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2)
+            verticalCenter: parent.verticalCenter
+        }
+
+        width: 40 * screenScaleFactor
+        text: sliderLabelRoot.value + startFrom // the current handle value, add 1 because layers is an array
+        horizontalAlignment: TextInput.AlignRight
+
+        // key bindings, work when label is currenctly focused (active handle in LayerSlider)
+        Keys.onUpPressed: sliderLabelRoot.setValue(sliderLabelRoot.value + ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
+        Keys.onDownPressed: sliderLabelRoot.setValue(sliderLabelRoot.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
+
+        style: TextFieldStyle {
+            textColor: UM.Theme.getColor("setting_control_text")
+            font: UM.Theme.getFont("default")
+            background: Item { }
+        }
+
+        onEditingFinished: {
+
+            // Ensure that the cursor is at the first position. On some systems the text isn't fully visible
+            // Seems to have to do something with different dpi densities that QML doesn't quite handle.
+            // Another option would be to increase the size even further, but that gives pretty ugly results.
+            cursorPosition = 0
+
+            if (valueLabel.text != "") {
+                // -startFrom because we need to convert back to an array structure
+                sliderLabelRoot.setValue(parseInt(valueLabel.text) - startFrom)
+            }
+        }
+
+        validator: IntValidator {
+            bottom:startFrom
+            top: sliderLabelRoot.maximumValue + startFrom // +startFrom because maybe we want to start in a different value rather than 0
+        }
+    }
+
+    BusyIndicator {
+        id: busyIndicator
+
+        anchors {
+            left: parent.right
+            leftMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2)
+            verticalCenter: parent.verticalCenter
+        }
+
+        width: sliderLabelRoot.height
+        height: width
+
+        visible: sliderLabelRoot.busy
+        running: sliderLabelRoot.busy
+    }
+}

+ 609 - 0
plugins/SimulationView/SimulationView.py

@@ -0,0 +1,609 @@
+# Copyright (c) 2017 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+import sys
+
+from PyQt5.QtCore import Qt
+from PyQt5.QtWidgets import QApplication
+
+from UM.Application import Application
+from UM.Event import Event, KeyEvent
+from UM.Job import Job
+from UM.Logger import Logger
+from UM.Math.Color import Color
+from UM.Mesh.MeshBuilder import MeshBuilder
+from UM.Message import Message
+from UM.PluginRegistry import PluginRegistry
+from UM.Preferences import Preferences
+from UM.Resources import Resources
+from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
+from UM.Scene.Selection import Selection
+from UM.Signal import Signal
+from UM.View.GL.OpenGL import OpenGL
+from UM.View.GL.OpenGLContext import OpenGLContext
+from UM.View.View import View
+from UM.i18n import i18nCatalog
+from cura.ConvexHullNode import ConvexHullNode
+
+from .NozzleNode import NozzleNode
+from .SimulationPass import SimulationPass
+from .SimulationViewProxy import SimulationViewProxy
+
+catalog = i18nCatalog("cura")
+
+import numpy
+import os.path
+
+## View used to display g-code paths.
+class SimulationView(View):
+    # Must match SimulationView.qml
+    LAYER_VIEW_TYPE_MATERIAL_TYPE = 0
+    LAYER_VIEW_TYPE_LINE_TYPE = 1
+    LAYER_VIEW_TYPE_FEEDRATE = 2
+    LAYER_VIEW_TYPE_THICKNESS = 3
+
+    def __init__(self):
+        super().__init__()
+
+        self._max_layers = 0
+        self._current_layer_num = 0
+        self._minimum_layer_num = 0
+        self._current_layer_mesh = None
+        self._current_layer_jumps = None
+        self._top_layers_job = None
+        self._activity = False
+        self._old_max_layers = 0
+
+        self._max_paths = 0
+        self._current_path_num = 0
+        self._minimum_path_num = 0
+        self.currentLayerNumChanged.connect(self._onCurrentLayerNumChanged)
+
+        self._busy = False
+        self._simulation_running = False
+
+        self._ghost_shader = None
+        self._layer_pass = None
+        self._composite_pass = None
+        self._old_layer_bindings = None
+        self._simulationview_composite_shader = None
+        self._old_composite_shader = None
+
+        self._global_container_stack = None
+        self._proxy = SimulationViewProxy()
+        self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged)
+
+        self._resetSettings()
+        self._legend_items = None
+        self._show_travel_moves = False
+        self._nozzle_node = None
+
+        Preferences.getInstance().addPreference("view/top_layer_count", 5)
+        Preferences.getInstance().addPreference("view/only_show_top_layers", False)
+        Preferences.getInstance().addPreference("view/force_layer_view_compatibility_mode", False)
+
+        Preferences.getInstance().addPreference("layerview/layer_view_type", 0)
+        Preferences.getInstance().addPreference("layerview/extruder_opacities", "")
+
+        Preferences.getInstance().addPreference("layerview/show_travel_moves", False)
+        Preferences.getInstance().addPreference("layerview/show_helpers", True)
+        Preferences.getInstance().addPreference("layerview/show_skin", True)
+        Preferences.getInstance().addPreference("layerview/show_infill", True)
+
+        Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
+        self._updateWithPreferences()
+
+        self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count"))
+        self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers"))
+        self._compatibility_mode = True  # for safety
+
+        self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"),
+                                                  title = catalog.i18nc("@info:title", "Simulation View"))
+
+    def _resetSettings(self):
+        self._layer_view_type = 0  # 0 is material color, 1 is color by linetype, 2 is speed
+        self._extruder_count = 0
+        self._extruder_opacity = [1.0, 1.0, 1.0, 1.0]
+        self._show_travel_moves = 0
+        self._show_helpers = 1
+        self._show_skin = 1
+        self._show_infill = 1
+        self.resetLayerData()
+
+    def getActivity(self):
+        return self._activity
+
+    def setActivity(self, activity):
+        if self._activity == activity:
+            return
+        self._activity = activity
+        self.activityChanged.emit()
+
+    def getSimulationPass(self):
+        if not self._layer_pass:
+            # Currently the RenderPass constructor requires a size > 0
+            # This should be fixed in RenderPass's constructor.
+            self._layer_pass = SimulationPass(1, 1)
+            self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode"))
+            self._layer_pass.setSimulationView(self)
+        return self._layer_pass
+
+    def getCurrentLayer(self):
+        return self._current_layer_num
+
+    def getMinimumLayer(self):
+        return self._minimum_layer_num
+
+    def getMaxLayers(self):
+        return self._max_layers
+
+    def getCurrentPath(self):
+        return self._current_path_num
+
+    def getMinimumPath(self):
+        return self._minimum_path_num
+
+    def getMaxPaths(self):
+        return self._max_paths
+
+    def getNozzleNode(self):
+        if not self._nozzle_node:
+            self._nozzle_node = NozzleNode()
+        return self._nozzle_node
+
+    def _onSceneChanged(self, node):
+        self.setActivity(False)
+        self.calculateMaxLayers()
+
+    def isBusy(self):
+        return self._busy
+
+    def setBusy(self, busy):
+        if busy != self._busy:
+            self._busy = busy
+            self.busyChanged.emit()
+
+    def isSimulationRunning(self):
+        return self._simulation_running
+
+    def setSimulationRunning(self, running):
+        self._simulation_running = running
+
+    def resetLayerData(self):
+        self._current_layer_mesh = None
+        self._current_layer_jumps = None
+        self._max_feedrate = sys.float_info.min
+        self._min_feedrate = sys.float_info.max
+        self._max_thickness = sys.float_info.min
+        self._min_thickness = sys.float_info.max
+
+    def beginRendering(self):
+        scene = self.getController().getScene()
+        renderer = self.getRenderer()
+
+        if not self._ghost_shader:
+            self._ghost_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader"))
+            self._ghost_shader.setUniformValue("u_color", Color(*Application.getInstance().getTheme().getColor("layerview_ghost").getRgb()))
+
+        for node in DepthFirstIterator(scene.getRoot()):
+            # We do not want to render ConvexHullNode as it conflicts with the bottom layers.
+            # However, it is somewhat relevant when the node is selected, so do render it then.
+            if type(node) is ConvexHullNode and not Selection.isSelected(node.getWatchedNode()):
+                continue
+
+            if not node.render(renderer):
+                if (node.getMeshData()) and node.isVisible():
+                    renderer.queueNode(node, transparent = True, shader = self._ghost_shader)
+
+    def setLayer(self, value):
+        if self._current_layer_num != value:
+            self._current_layer_num = value
+            if self._current_layer_num < 0:
+                self._current_layer_num = 0
+            if self._current_layer_num > self._max_layers:
+                self._current_layer_num = self._max_layers
+            if self._current_layer_num < self._minimum_layer_num:
+                self._minimum_layer_num = self._current_layer_num
+
+            self._startUpdateTopLayers()
+
+            self.currentLayerNumChanged.emit()
+
+    def setMinimumLayer(self, value):
+        if self._minimum_layer_num != value:
+            self._minimum_layer_num = value
+            if self._minimum_layer_num < 0:
+                self._minimum_layer_num = 0
+            if self._minimum_layer_num > self._max_layers:
+                self._minimum_layer_num = self._max_layers
+            if self._minimum_layer_num > self._current_layer_num:
+                self._current_layer_num = self._minimum_layer_num
+
+            self._startUpdateTopLayers()
+
+            self.currentLayerNumChanged.emit()
+
+    def setPath(self, value):
+        if self._current_path_num != value:
+            self._current_path_num = value
+            if self._current_path_num < 0:
+                self._current_path_num = 0
+            if self._current_path_num > self._max_paths:
+                self._current_path_num = self._max_paths
+            if self._current_path_num < self._minimum_path_num:
+                self._minimum_path_num = self._current_path_num
+
+            self._startUpdateTopLayers()
+
+            self.currentPathNumChanged.emit()
+
+    def setMinimumPath(self, value):
+        if self._minimum_path_num != value:
+            self._minimum_path_num = value
+            if self._minimum_path_num < 0:
+                self._minimum_path_num = 0
+            if self._minimum_path_num > self._max_layers:
+                self._minimum_path_num = self._max_layers
+            if self._minimum_path_num > self._current_path_num:
+                self._current_path_num = self._minimum_path_num
+
+            self._startUpdateTopLayers()
+
+            self.currentPathNumChanged.emit()
+
+    ##  Set the layer view type
+    #
+    #   \param layer_view_type integer as in SimulationView.qml and this class
+    def setSimulationViewType(self, layer_view_type):
+        self._layer_view_type = layer_view_type
+        self.currentLayerNumChanged.emit()
+
+    ##  Return the layer view type, integer as in SimulationView.qml and this class
+    def getSimulationViewType(self):
+        return self._layer_view_type
+
+    ##  Set the extruder opacity
+    #
+    #   \param extruder_nr 0..3
+    #   \param opacity 0.0 .. 1.0
+    def setExtruderOpacity(self, extruder_nr, opacity):
+        if 0 <= extruder_nr <= 3:
+            self._extruder_opacity[extruder_nr] = opacity
+            self.currentLayerNumChanged.emit()
+
+    def getExtruderOpacities(self):
+        return self._extruder_opacity
+
+    def setShowTravelMoves(self, show):
+        self._show_travel_moves = show
+        self.currentLayerNumChanged.emit()
+
+    def getShowTravelMoves(self):
+        return self._show_travel_moves
+
+    def setShowHelpers(self, show):
+        self._show_helpers = show
+        self.currentLayerNumChanged.emit()
+
+    def getShowHelpers(self):
+        return self._show_helpers
+
+    def setShowSkin(self, show):
+        self._show_skin = show
+        self.currentLayerNumChanged.emit()
+
+    def getShowSkin(self):
+        return self._show_skin
+
+    def setShowInfill(self, show):
+        self._show_infill = show
+        self.currentLayerNumChanged.emit()
+
+    def getShowInfill(self):
+        return self._show_infill
+
+    def getCompatibilityMode(self):
+        return self._compatibility_mode
+
+    def getExtruderCount(self):
+        return self._extruder_count
+
+    def getMinFeedrate(self):
+        return self._min_feedrate
+
+    def getMaxFeedrate(self):
+        return self._max_feedrate
+
+    def getMinThickness(self):
+        return self._min_thickness
+
+    def getMaxThickness(self):
+        return self._max_thickness
+
+    def calculateMaxLayers(self):
+        scene = self.getController().getScene()
+
+        self._old_max_layers = self._max_layers
+        ## Recalculate num max layers
+        new_max_layers = 0
+        for node in DepthFirstIterator(scene.getRoot()):
+            layer_data = node.callDecoration("getLayerData")
+            if not layer_data:
+                continue
+
+            self.setActivity(True)
+            min_layer_number = sys.maxsize
+            max_layer_number = -sys.maxsize
+            for layer_id in layer_data.getLayers():
+                # Store the max and min feedrates and thicknesses for display purposes
+                for p in layer_data.getLayer(layer_id).polygons:
+                    self._max_feedrate = max(float(p.lineFeedrates.max()), self._max_feedrate)
+                    self._min_feedrate = min(float(p.lineFeedrates.min()), self._min_feedrate)
+                    self._max_thickness = max(float(p.lineThicknesses.max()), self._max_thickness)
+                    self._min_thickness = min(float(p.lineThicknesses.min()), self._min_thickness)
+                if max_layer_number < layer_id:
+                    max_layer_number = layer_id
+                if min_layer_number > layer_id:
+                    min_layer_number = layer_id
+            layer_count = max_layer_number - min_layer_number
+
+            if new_max_layers < layer_count:
+                new_max_layers = layer_count
+
+        if new_max_layers > 0 and new_max_layers != self._old_max_layers:
+            self._max_layers = new_max_layers
+
+            # The qt slider has a bit of weird behavior that if the maxvalue needs to be changed first
+            # if it's the largest value. If we don't do this, we can have a slider block outside of the
+            # slider.
+            if new_max_layers > self._current_layer_num:
+                self.maxLayersChanged.emit()
+                self.setLayer(int(self._max_layers))
+            else:
+                self.setLayer(int(self._max_layers))
+                self.maxLayersChanged.emit()
+        self._startUpdateTopLayers()
+
+    def calculateMaxPathsOnLayer(self, layer_num):
+        # Update the currentPath
+        scene = self.getController().getScene()
+        for node in DepthFirstIterator(scene.getRoot()):
+            layer_data = node.callDecoration("getLayerData")
+            if not layer_data:
+                continue
+
+            layer = layer_data.getLayer(layer_num)
+            if layer is None:
+                return
+            new_max_paths = layer.lineMeshElementCount()
+            if new_max_paths > 0 and new_max_paths != self._max_paths:
+                self._max_paths = new_max_paths
+                self.maxPathsChanged.emit()
+
+            self.setPath(int(new_max_paths))
+
+    maxLayersChanged = Signal()
+    maxPathsChanged = Signal()
+    currentLayerNumChanged = Signal()
+    currentPathNumChanged = Signal()
+    globalStackChanged = Signal()
+    preferencesChanged = Signal()
+    busyChanged = Signal()
+    activityChanged = Signal()
+
+    ##  Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created
+    #   as this caused some issues.
+    def getProxy(self, engine, script_engine):
+        return self._proxy
+
+    def endRendering(self):
+        pass
+
+    def event(self, event):
+        modifiers = QApplication.keyboardModifiers()
+        ctrl_is_active = modifiers & Qt.ControlModifier
+        shift_is_active = modifiers & Qt.ShiftModifier
+        if event.type == Event.KeyPressEvent and ctrl_is_active:
+            amount = 10 if shift_is_active else 1
+            if event.key == KeyEvent.UpKey:
+                self.setLayer(self._current_layer_num + amount)
+                return True
+            if event.key == KeyEvent.DownKey:
+                self.setLayer(self._current_layer_num - amount)
+                return True
+
+        if event.type == Event.ViewActivateEvent:
+            # Make sure the SimulationPass is created
+            layer_pass = self.getSimulationPass()
+            self.getRenderer().addRenderPass(layer_pass)
+
+            # Make sure the NozzleNode is add to the root
+            nozzle = self.getNozzleNode()
+            nozzle.setParent(self.getController().getScene().getRoot())
+            nozzle.setVisible(False)
+
+            Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
+            self._onGlobalStackChanged()
+
+            if not self._simulationview_composite_shader:
+                self._simulationview_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), "simulationview_composite.shader"))
+                theme = Application.getInstance().getTheme()
+                self._simulationview_composite_shader.setUniformValue("u_background_color", Color(*theme.getColor("viewport_background").getRgb()))
+                self._simulationview_composite_shader.setUniformValue("u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb()))
+
+            if not self._composite_pass:
+                self._composite_pass = self.getRenderer().getRenderPass("composite")
+
+            self._old_layer_bindings = self._composite_pass.getLayerBindings()[:] # make a copy so we can restore to it later
+            self._composite_pass.getLayerBindings().append("simulationview")
+            self._old_composite_shader = self._composite_pass.getCompositeShader()
+            self._composite_pass.setCompositeShader(self._simulationview_composite_shader)
+
+        elif event.type == Event.ViewDeactivateEvent:
+            self._wireprint_warning_message.hide()
+            Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged)
+            if self._global_container_stack:
+                self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
+
+            self._nozzle_node.setParent(None)
+            self.getRenderer().removeRenderPass(self._layer_pass)
+            self._composite_pass.setLayerBindings(self._old_layer_bindings)
+            self._composite_pass.setCompositeShader(self._old_composite_shader)
+
+    def getCurrentLayerMesh(self):
+        return self._current_layer_mesh
+
+    def getCurrentLayerJumps(self):
+        return self._current_layer_jumps
+
+    def _onGlobalStackChanged(self):
+        if self._global_container_stack:
+            self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
+        self._global_container_stack = Application.getInstance().getGlobalContainerStack()
+        if self._global_container_stack:
+            self._global_container_stack.propertyChanged.connect(self._onPropertyChanged)
+            self._extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
+            self._onPropertyChanged("wireframe_enabled", "value")
+            self.globalStackChanged.emit()
+        else:
+            self._wireprint_warning_message.hide()
+
+    def _onPropertyChanged(self, key, property_name):
+        if key == "wireframe_enabled" and property_name == "value":
+            if self._global_container_stack.getProperty("wireframe_enabled", "value"):
+                self._wireprint_warning_message.show()
+            else:
+                self._wireprint_warning_message.hide()
+
+    def _onCurrentLayerNumChanged(self):
+        self.calculateMaxPathsOnLayer(self._current_layer_num)
+
+    def _startUpdateTopLayers(self):
+        if not self._compatibility_mode:
+            return
+
+        if self._top_layers_job:
+            self._top_layers_job.finished.disconnect(self._updateCurrentLayerMesh)
+            self._top_layers_job.cancel()
+
+        self.setBusy(True)
+
+        self._top_layers_job = _CreateTopLayersJob(self._controller.getScene(), self._current_layer_num, self._solid_layers)
+        self._top_layers_job.finished.connect(self._updateCurrentLayerMesh)
+        self._top_layers_job.start()
+
+    def _updateCurrentLayerMesh(self, job):
+        self.setBusy(False)
+
+        if not job.getResult():
+            return
+        self.resetLayerData()  # Reset the layer data only when job is done. Doing it now prevents "blinking" data.
+        self._current_layer_mesh = job.getResult().get("layers")
+        if self._show_travel_moves:
+            self._current_layer_jumps = job.getResult().get("jumps")
+        self._controller.getScene().sceneChanged.emit(self._controller.getScene().getRoot())
+
+        self._top_layers_job = None
+
+    def _updateWithPreferences(self):
+        self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count"))
+        self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers"))
+        self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool(
+            Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode"))
+
+        self.setSimulationViewType(int(float(Preferences.getInstance().getValue("layerview/layer_view_type"))));
+
+        for extruder_nr, extruder_opacity in enumerate(Preferences.getInstance().getValue("layerview/extruder_opacities").split("|")):
+            try:
+                opacity = float(extruder_opacity)
+            except ValueError:
+                opacity = 1.0
+            self.setExtruderOpacity(extruder_nr, opacity)
+
+        self.setShowTravelMoves(bool(Preferences.getInstance().getValue("layerview/show_travel_moves")))
+        self.setShowHelpers(bool(Preferences.getInstance().getValue("layerview/show_helpers")))
+        self.setShowSkin(bool(Preferences.getInstance().getValue("layerview/show_skin")))
+        self.setShowInfill(bool(Preferences.getInstance().getValue("layerview/show_infill")))
+
+        self._startUpdateTopLayers()
+        self.preferencesChanged.emit()
+
+    def _onPreferencesChanged(self, preference):
+        if preference not in {
+            "view/top_layer_count",
+            "view/only_show_top_layers",
+            "view/force_layer_view_compatibility_mode",
+            "layerview/layer_view_type",
+            "layerview/extruder_opacities",
+            "layerview/show_travel_moves",
+            "layerview/show_helpers",
+            "layerview/show_skin",
+            "layerview/show_infill",
+            }:
+            return
+
+        self._updateWithPreferences()
+
+
+class _CreateTopLayersJob(Job):
+    def __init__(self, scene, layer_number, solid_layers):
+        super().__init__()
+
+        self._scene = scene
+        self._layer_number = layer_number
+        self._solid_layers = solid_layers
+        self._cancel = False
+
+    def run(self):
+        layer_data = None
+        for node in DepthFirstIterator(self._scene.getRoot()):
+            layer_data = node.callDecoration("getLayerData")
+            if layer_data:
+                break
+
+        if self._cancel or not layer_data:
+            return
+
+        layer_mesh = MeshBuilder()
+        for i in range(self._solid_layers):
+            layer_number = self._layer_number - i
+            if layer_number < 0:
+                continue
+
+            try:
+                layer = layer_data.getLayer(layer_number).createMesh()
+            except Exception:
+                Logger.logException("w", "An exception occurred while creating layer mesh.")
+                return
+
+            if not layer or layer.getVertices() is None:
+                continue
+
+            layer_mesh.addIndices(layer_mesh.getVertexCount() + layer.getIndices())
+            layer_mesh.addVertices(layer.getVertices())
+
+            # Scale layer color by a brightness factor based on the current layer number
+            # This will result in a range of 0.5 - 1.0 to multiply colors by.
+            brightness = numpy.ones((1, 4), dtype=numpy.float32) * (2.0 - (i / self._solid_layers)) / 2.0
+            brightness[0, 3] = 1.0
+            layer_mesh.addColors(layer.getColors() * brightness)
+
+            if self._cancel:
+                return
+
+            Job.yieldThread()
+
+        if self._cancel:
+            return
+
+        Job.yieldThread()
+        jump_mesh = layer_data.getLayer(self._layer_number).createJumps()
+        if not jump_mesh or jump_mesh.getVertices() is None:
+            jump_mesh = None
+
+        self.setResult({"layers": layer_mesh.build(), "jumps": jump_mesh})
+
+    def cancel(self):
+        self._cancel = True
+        super().cancel()
+

+ 645 - 0
plugins/SimulationView/SimulationView.qml

@@ -0,0 +1,645 @@
+// Copyright (c) 2017 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.4
+import QtQuick.Controls 1.2
+import QtQuick.Layouts 1.1
+import QtQuick.Controls.Styles 1.1
+
+import UM 1.0 as UM
+import Cura 1.0 as Cura
+
+Item
+{
+    id: base
+    width: {
+        if (UM.SimulationView.compatibilityMode) {
+            return UM.Theme.getSize("layerview_menu_size_compatibility").width;
+        } else {
+            return UM.Theme.getSize("layerview_menu_size").width;
+        }
+    }
+    height: {
+        if (UM.SimulationView.compatibilityMode) {
+            return UM.Theme.getSize("layerview_menu_size_compatibility").height;
+        } else if (UM.Preferences.getValue("layerview/layer_view_type") == 0) {
+            return UM.Theme.getSize("layerview_menu_size_material_color_mode").height + UM.SimulationView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height)
+        } else {
+            return UM.Theme.getSize("layerview_menu_size").height + UM.SimulationView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height)
+        }
+    }
+
+    property var buttonTarget: {
+        if(parent != null)
+        {
+            var force_binding = parent.y; // ensure this gets reevaluated when the panel moves
+            return base.mapFromItem(parent.parent, parent.buttonTarget.x, parent.buttonTarget.y)
+        }
+        return Qt.point(0,0)
+    }
+
+    visible: parent != null ? !parent.parent.monitoringPrint: true
+
+    UM.PointingRectangle {
+        id: layerViewMenu
+        anchors.right: parent.right
+        anchors.top: parent.top
+        width: parent.width
+        height: parent.height
+        z: layerSlider.z - 1
+        color: UM.Theme.getColor("tool_panel_background")
+        borderWidth: UM.Theme.getSize("default_lining").width
+        borderColor: UM.Theme.getColor("lining")
+        arrowSize: 0 // hide arrow until weird issue with first time rendering is fixed
+
+        ColumnLayout {
+            id: view_settings
+
+            property var extruder_opacities: UM.Preferences.getValue("layerview/extruder_opacities").split("|")
+            property bool show_travel_moves: UM.Preferences.getValue("layerview/show_travel_moves")
+            property bool show_helpers: UM.Preferences.getValue("layerview/show_helpers")
+            property bool show_skin: UM.Preferences.getValue("layerview/show_skin")
+            property bool show_infill: UM.Preferences.getValue("layerview/show_infill")
+            // if we are in compatibility mode, we only show the "line type"
+            property bool show_legend: UM.SimulationView.compatibilityMode ? true : UM.Preferences.getValue("layerview/layer_view_type") == 1
+            property bool show_gradient: UM.SimulationView.compatibilityMode ? false : UM.Preferences.getValue("layerview/layer_view_type") == 2 || UM.Preferences.getValue("layerview/layer_view_type") == 3
+            property bool only_show_top_layers: UM.Preferences.getValue("view/only_show_top_layers")
+            property int top_layer_count: UM.Preferences.getValue("view/top_layer_count")
+
+            anchors.top: parent.top
+            anchors.topMargin: UM.Theme.getSize("default_margin").height
+            anchors.left: parent.left
+            anchors.leftMargin: UM.Theme.getSize("default_margin").width
+            spacing: UM.Theme.getSize("layerview_row_spacing").height
+            anchors.right: parent.right
+            anchors.rightMargin: UM.Theme.getSize("default_margin").width
+
+            Label
+            {
+                id: layerViewTypesLabel
+                anchors.left: parent.left
+                text: catalog.i18nc("@label","Color scheme")
+                font: UM.Theme.getFont("default");
+                visible: !UM.SimulationView.compatibilityMode
+                Layout.fillWidth: true
+                color: UM.Theme.getColor("setting_control_text")
+            }
+
+            ListModel  // matches SimulationView.py
+            {
+                id: layerViewTypes
+            }
+
+            Component.onCompleted:
+            {
+                layerViewTypes.append({
+                    text: catalog.i18nc("@label:listbox", "Material Color"),
+                    type_id: 0
+                })
+                layerViewTypes.append({
+                    text: catalog.i18nc("@label:listbox", "Line Type"),
+                    type_id: 1
+                })
+                layerViewTypes.append({
+                    text: catalog.i18nc("@label:listbox", "Feedrate"),
+                    type_id: 2
+                })
+                layerViewTypes.append({
+                    text: catalog.i18nc("@label:listbox", "Layer thickness"),
+                    type_id: 3  // these ids match the switching in the shader
+                })
+            }
+
+            ComboBox
+            {
+                id: layerTypeCombobox
+                anchors.left: parent.left
+                Layout.fillWidth: true
+                Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
+                model: layerViewTypes
+                visible: !UM.SimulationView.compatibilityMode
+                style: UM.Theme.styles.combobox
+                anchors.right: parent.right
+                anchors.rightMargin: 10 * screenScaleFactor
+
+                onActivated:
+                {
+                    UM.Preferences.setValue("layerview/layer_view_type", index);
+                }
+
+                Component.onCompleted:
+                {
+                    currentIndex = UM.SimulationView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type");
+                    updateLegends(currentIndex);
+                }
+
+                function updateLegends(type_id)
+                {
+                    // update visibility of legends
+                    view_settings.show_legend = UM.SimulationView.compatibilityMode || (type_id == 1);
+                }
+
+            }
+
+            Label
+            {
+                id: compatibilityModeLabel
+                anchors.left: parent.left
+                text: catalog.i18nc("@label","Compatibility Mode")
+                font: UM.Theme.getFont("default")
+                color: UM.Theme.getColor("text")
+                visible: UM.SimulationView.compatibilityMode
+                Layout.fillWidth: true
+                Layout.preferredHeight: UM.Theme.getSize("layerview_row").height
+                Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
+            }
+
+            Label
+            {
+                id: space2Label
+                anchors.left: parent.left
+                text: " "
+                font.pointSize: 0.5
+            }
+
+            Connections {
+                target: UM.Preferences
+                onPreferenceChanged:
+                {
+                    layerTypeCombobox.currentIndex = UM.SimulationView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type");
+                    layerTypeCombobox.updateLegends(layerTypeCombobox.currentIndex);
+                    view_settings.extruder_opacities = UM.Preferences.getValue("layerview/extruder_opacities").split("|");
+                    view_settings.show_travel_moves = UM.Preferences.getValue("layerview/show_travel_moves");
+                    view_settings.show_helpers = UM.Preferences.getValue("layerview/show_helpers");
+                    view_settings.show_skin = UM.Preferences.getValue("layerview/show_skin");
+                    view_settings.show_infill = UM.Preferences.getValue("layerview/show_infill");
+                    view_settings.only_show_top_layers = UM.Preferences.getValue("view/only_show_top_layers");
+                    view_settings.top_layer_count = UM.Preferences.getValue("view/top_layer_count");
+                }
+            }
+
+            Repeater {
+                model: Cura.ExtrudersModel{}
+                CheckBox {
+                    id: extrudersModelCheckBox
+                    checked: view_settings.extruder_opacities[index] > 0.5 || view_settings.extruder_opacities[index] == undefined || view_settings.extruder_opacities[index] == ""
+                    onClicked: {
+                        view_settings.extruder_opacities[index] = checked ? 1.0 : 0.0
+                        UM.Preferences.setValue("layerview/extruder_opacities", view_settings.extruder_opacities.join("|"));
+                    }
+                    visible: !UM.SimulationView.compatibilityMode
+                    enabled: index + 1 <= 4
+                    Rectangle {
+                        anchors.verticalCenter: parent.verticalCenter
+                        anchors.right: extrudersModelCheckBox.right
+                        anchors.rightMargin: UM.Theme.getSize("default_margin").width
+                        width: UM.Theme.getSize("layerview_legend_size").width
+                        height: UM.Theme.getSize("layerview_legend_size").height
+                        color: model.color
+                        radius: width / 2
+                        border.width: UM.Theme.getSize("default_lining").width
+                        border.color: UM.Theme.getColor("lining")
+                        visible: !view_settings.show_legend & !view_settings.show_gradient
+                    }
+                    Layout.fillWidth: true
+                    Layout.preferredHeight: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
+                    Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
+                    style: UM.Theme.styles.checkbox
+                    Label
+                    {
+                        text: model.name
+                        elide: Text.ElideRight
+                        color: UM.Theme.getColor("setting_control_text")
+                        font: UM.Theme.getFont("default")
+                        anchors.verticalCenter: parent.verticalCenter
+                        anchors.left: extrudersModelCheckBox.left;
+                        anchors.right: extrudersModelCheckBox.right;
+                        anchors.leftMargin: UM.Theme.getSize("checkbox").width + UM.Theme.getSize("default_margin").width /2
+                        anchors.rightMargin: UM.Theme.getSize("default_margin").width * 2
+                    }
+                }
+            }
+
+            Repeater {
+                model: ListModel {
+                    id: typesLegendModel
+                    Component.onCompleted:
+                    {
+                        typesLegendModel.append({
+                            label: catalog.i18nc("@label", "Show Travels"),
+                            initialValue: view_settings.show_travel_moves,
+                            preference: "layerview/show_travel_moves",
+                            colorId:  "layerview_move_combing"
+                        });
+                        typesLegendModel.append({
+                            label: catalog.i18nc("@label", "Show Helpers"),
+                            initialValue: view_settings.show_helpers,
+                            preference: "layerview/show_helpers",
+                            colorId:  "layerview_support"
+                        });
+                        typesLegendModel.append({
+                            label: catalog.i18nc("@label", "Show Shell"),
+                            initialValue: view_settings.show_skin,
+                            preference: "layerview/show_skin",
+                            colorId:  "layerview_inset_0"
+                        });
+                        typesLegendModel.append({
+                            label: catalog.i18nc("@label", "Show Infill"),
+                            initialValue: view_settings.show_infill,
+                            preference: "layerview/show_infill",
+                            colorId:  "layerview_infill"
+                        });
+                    }
+                }
+
+                CheckBox {
+                    id: legendModelCheckBox
+                    checked: model.initialValue
+                    onClicked: {
+                        UM.Preferences.setValue(model.preference, checked);
+                    }
+                    Rectangle {
+                        anchors.verticalCenter: parent.verticalCenter
+                        anchors.right: legendModelCheckBox.right
+                        anchors.rightMargin: UM.Theme.getSize("default_margin").width
+                        width: UM.Theme.getSize("layerview_legend_size").width
+                        height: UM.Theme.getSize("layerview_legend_size").height
+                        color: UM.Theme.getColor(model.colorId)
+                        border.width: UM.Theme.getSize("default_lining").width
+                        border.color: UM.Theme.getColor("lining")
+                        visible: view_settings.show_legend
+                    }
+                    Layout.fillWidth: true
+                    Layout.preferredHeight: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
+                    Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
+                    style: UM.Theme.styles.checkbox
+                    Label
+                    {
+                        text: label
+                        font: UM.Theme.getFont("default")
+                        elide: Text.ElideRight
+                        color: UM.Theme.getColor("setting_control_text")
+                        anchors.verticalCenter: parent.verticalCenter
+                        anchors.left: legendModelCheckBox.left;
+                        anchors.right: legendModelCheckBox.right;
+                        anchors.leftMargin: UM.Theme.getSize("checkbox").width + UM.Theme.getSize("default_margin").width /2
+                        anchors.rightMargin: UM.Theme.getSize("default_margin").width * 2
+                    }
+                }
+            }
+
+            CheckBox {
+                checked: view_settings.only_show_top_layers
+                onClicked: {
+                    UM.Preferences.setValue("view/only_show_top_layers", checked ? 1.0 : 0.0);
+                }
+                text: catalog.i18nc("@label", "Only Show Top Layers")
+                visible: UM.SimulationView.compatibilityMode
+                style: UM.Theme.styles.checkbox
+            }
+            CheckBox {
+                checked: view_settings.top_layer_count == 5
+                onClicked: {
+                    UM.Preferences.setValue("view/top_layer_count", checked ? 5 : 1);
+                }
+                text: catalog.i18nc("@label", "Show 5 Detailed Layers On Top")
+                visible: UM.SimulationView.compatibilityMode
+                style: UM.Theme.styles.checkbox
+            }
+
+            Repeater {
+                model: ListModel {
+                    id: typesLegendModelNoCheck
+                    Component.onCompleted:
+                    {
+                        typesLegendModelNoCheck.append({
+                            label: catalog.i18nc("@label", "Top / Bottom"),
+                            colorId: "layerview_skin",
+                        });
+                        typesLegendModelNoCheck.append({
+                            label: catalog.i18nc("@label", "Inner Wall"),
+                            colorId: "layerview_inset_x",
+                        });
+                    }
+                }
+
+                Label {
+                    text: label
+                    visible: view_settings.show_legend
+                    id: typesLegendModelLabel
+                    Rectangle {
+                        anchors.verticalCenter: parent.verticalCenter
+                        anchors.right: typesLegendModelLabel.right
+                        anchors.rightMargin: UM.Theme.getSize("default_margin").width
+                        width: UM.Theme.getSize("layerview_legend_size").width
+                        height: UM.Theme.getSize("layerview_legend_size").height
+                        color: UM.Theme.getColor(model.colorId)
+                        border.width: UM.Theme.getSize("default_lining").width
+                        border.color: UM.Theme.getColor("lining")
+                        visible: view_settings.show_legend
+                    }
+                    Layout.fillWidth: true
+                    Layout.preferredHeight: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
+                    Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
+                    color: UM.Theme.getColor("setting_control_text")
+                    font: UM.Theme.getFont("default")
+                }
+            }
+
+            // Text for the minimum, maximum and units for the feedrates and layer thickness
+            Rectangle {
+                id: gradientLegend
+                visible: view_settings.show_gradient
+                width: parent.width
+                height: UM.Theme.getSize("layerview_row").height
+                anchors {
+                    topMargin: UM.Theme.getSize("slider_layerview_margin").height
+                    horizontalCenter: parent.horizontalCenter
+                }
+
+                Label {
+                    text: minText()
+                    anchors.left: parent.left
+                    color: UM.Theme.getColor("setting_control_text")
+                    font: UM.Theme.getFont("default")
+
+                    function minText() {
+                        if (UM.SimulationView.layerActivity && CuraApplication.platformActivity) {
+                            // Feedrate selected
+                            if (UM.Preferences.getValue("layerview/layer_view_type") == 2) {
+                                return parseFloat(UM.SimulationView.getMinFeedrate()).toFixed(2)
+                            }
+                            // Layer thickness selected
+                            if (UM.Preferences.getValue("layerview/layer_view_type") == 3) {
+                                return parseFloat(UM.SimulationView.getMinThickness()).toFixed(2)
+                            }
+                        }
+                        return catalog.i18nc("@label","min")
+                    }
+                }
+
+                Label {
+                    text: unitsText()
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    color: UM.Theme.getColor("setting_control_text")
+                    font: UM.Theme.getFont("default")
+
+                    function unitsText() {
+                        if (UM.SimulationView.layerActivity && CuraApplication.platformActivity) {
+                            // Feedrate selected
+                            if (UM.Preferences.getValue("layerview/layer_view_type") == 2) {
+                                return "mm/s"
+                            }
+                            // Layer thickness selected
+                            if (UM.Preferences.getValue("layerview/layer_view_type") == 3) {
+                                return "mm"
+                            }
+                        }
+                        return ""
+                    }
+                }
+
+                Label {
+                    text: maxText()
+                    anchors.right: parent.right
+                    color: UM.Theme.getColor("setting_control_text")
+                    font: UM.Theme.getFont("default")
+
+                    function maxText() {
+                        if (UM.SimulationView.layerActivity && CuraApplication.platformActivity) {
+                            // Feedrate selected
+                            if (UM.Preferences.getValue("layerview/layer_view_type") == 2) {
+                                return parseFloat(UM.SimulationView.getMaxFeedrate()).toFixed(2)
+                            }
+                            // Layer thickness selected
+                            if (UM.Preferences.getValue("layerview/layer_view_type") == 3) {
+                                return parseFloat(UM.SimulationView.getMaxThickness()).toFixed(2)
+                            }
+                        }
+                        return catalog.i18nc("@label","max")
+                    }
+                }
+            }
+
+            // Gradient colors for feedrate and thickness
+            Rectangle { // In QML 5.9 can be changed by LinearGradient
+                // Invert values because then the bar is rotated 90 degrees
+                id: gradient
+                visible: view_settings.show_gradient
+                anchors.left: parent.right
+                height: parent.width
+                width: UM.Theme.getSize("layerview_row").height * 1.5
+                border.width: UM.Theme.getSize("default_lining").width
+                border.color: UM.Theme.getColor("lining")
+                transform: Rotation {origin.x: 0; origin.y: 0; angle: 90}
+                gradient: Gradient {
+                    GradientStop {
+                        position: 0.000
+                        color: Qt.rgba(1, 0, 0, 1)
+                    }
+                    GradientStop {
+                        position: 0.25
+                        color: Qt.rgba(0.75, 0.5, 0.25, 1)
+                    }
+                    GradientStop {
+                        position: 0.5
+                        color: Qt.rgba(0.5, 1, 0.5, 1)
+                    }
+                    GradientStop {
+                        position: 0.75
+                        color: Qt.rgba(0.25, 0.5, 0.75, 1)
+                    }
+                    GradientStop {
+                        position: 1.0
+                        color: Qt.rgba(0, 0, 1, 1)
+                    }
+                }
+            }
+        }
+
+        Item {
+            id: slidersBox
+
+            width: parent.width
+            visible: UM.SimulationView.layerActivity && CuraApplication.platformActivity
+
+            anchors {
+                top: parent.bottom
+                topMargin: UM.Theme.getSize("slider_layerview_margin").height
+                left: parent.left
+            }
+
+            PathSlider {
+                id: pathSlider
+
+                width: parent.width
+                height: UM.Theme.getSize("slider_handle").width
+                anchors.left: parent.left
+                visible: !UM.SimulationView.compatibilityMode
+
+                // custom properties
+                handleValue: UM.SimulationView.currentPath
+                maximumValue: UM.SimulationView.numPaths
+                handleSize: UM.Theme.getSize("slider_handle").width
+                trackThickness: UM.Theme.getSize("slider_groove").width
+                trackColor: UM.Theme.getColor("slider_groove")
+                trackBorderColor: UM.Theme.getColor("slider_groove_border")
+                handleColor: UM.Theme.getColor("slider_handle")
+                handleActiveColor: UM.Theme.getColor("slider_handle_active")
+                rangeColor: UM.Theme.getColor("slider_groove_fill")
+
+                // update values when layer data changes
+                Connections {
+                    target: UM.SimulationView
+                    onMaxPathsChanged: pathSlider.setHandleValue(UM.SimulationView.currentPath)
+                    onCurrentPathChanged: pathSlider.setHandleValue(UM.SimulationView.currentPath)
+                }
+
+                // make sure the slider handlers show the correct value after switching views
+                Component.onCompleted: {
+                    pathSlider.setHandleValue(UM.SimulationView.currentPath)
+                }
+            }
+
+            LayerSlider {
+                id: layerSlider
+
+                width: UM.Theme.getSize("slider_handle").width
+                height: UM.Theme.getSize("layerview_menu_size").height
+
+                anchors {
+                    top: pathSlider.bottom
+                    topMargin: UM.Theme.getSize("slider_layerview_margin").height
+                    right: parent.right
+                    rightMargin: UM.Theme.getSize("slider_layerview_margin").width
+                }
+
+                // custom properties
+                upperValue: UM.SimulationView.currentLayer
+                lowerValue: UM.SimulationView.minimumLayer
+                maximumValue: UM.SimulationView.numLayers
+                handleSize: UM.Theme.getSize("slider_handle").width
+                trackThickness: UM.Theme.getSize("slider_groove").width
+                trackColor: UM.Theme.getColor("slider_groove")
+                trackBorderColor: UM.Theme.getColor("slider_groove_border")
+                upperHandleColor: UM.Theme.getColor("slider_handle")
+                lowerHandleColor: UM.Theme.getColor("slider_handle")
+                rangeHandleColor: UM.Theme.getColor("slider_groove_fill")
+                handleActiveColor: UM.Theme.getColor("slider_handle_active")
+                handleLabelWidth: UM.Theme.getSize("slider_layerview_background").width
+
+                // update values when layer data changes
+                Connections {
+                    target: UM.SimulationView
+                    onMaxLayersChanged: layerSlider.setUpperValue(UM.SimulationView.currentLayer)
+                    onMinimumLayerChanged: layerSlider.setLowerValue(UM.SimulationView.minimumLayer)
+                    onCurrentLayerChanged: layerSlider.setUpperValue(UM.SimulationView.currentLayer)
+                }
+
+                // make sure the slider handlers show the correct value after switching views
+                Component.onCompleted: {
+                    layerSlider.setLowerValue(UM.SimulationView.minimumLayer)
+                    layerSlider.setUpperValue(UM.SimulationView.currentLayer)
+                }
+            }
+
+            // Play simulation button
+            Button {
+                id: playButton
+                implicitWidth: UM.Theme.getSize("button").width * 0.75;
+                implicitHeight: UM.Theme.getSize("button").height * 0.75;
+                iconSource: "./resources/simulation_resume.svg"
+                style: UM.Theme.styles.tool_button
+                visible: !UM.SimulationView.compatibilityMode
+                anchors {
+                    horizontalCenter: layerSlider.horizontalCenter
+                    top: layerSlider.bottom
+                    topMargin: UM.Theme.getSize("slider_layerview_margin").width
+                }
+
+                property var status: 0  // indicates if it's stopped (0) or playing (1)
+
+                onClicked: {
+                    switch(status) {
+                        case 0: {
+                            resumeSimulation()
+                            break
+                        }
+                        case 1: {
+                            pauseSimulation()
+                            break
+                        }
+                    }
+                }
+
+                function pauseSimulation() {
+                    UM.SimulationView.setSimulationRunning(false)
+                    iconSource = "./resources/simulation_resume.svg"
+                    simulationTimer.stop()
+                    status = 0
+                }
+
+                function resumeSimulation() {
+                    UM.SimulationView.setSimulationRunning(true)
+                    iconSource = "./resources/simulation_pause.svg"
+                    simulationTimer.start()
+                }
+            }
+        }
+
+        Timer
+        {
+            id: simulationTimer
+            interval: 250
+            running: false
+            repeat: true
+            onTriggered: {
+                var currentPath = UM.SimulationView.currentPath
+                var numPaths = UM.SimulationView.numPaths
+                var currentLayer = UM.SimulationView.currentLayer
+                var numLayers = UM.SimulationView.numLayers
+                // When the user plays the simulation, if the path slider is at the end of this layer, we start
+                // the simulation at the beginning of the current layer.
+                if (playButton.status == 0)
+                {
+                    if (currentPath >= numPaths)
+                    {
+                        UM.SimulationView.setCurrentPath(0)
+                    }
+                    else
+                    {
+                        UM.SimulationView.setCurrentPath(currentPath+1)
+                    }
+                }
+                // If the simulation is already playing and we reach the end of a layer, then it automatically
+                // starts at the beginning of the next layer.
+                else
+                {
+                    if (currentPath >= numPaths)
+                    {
+                        // At the end of the model, the simulation stops
+                        if (currentLayer >= numLayers)
+                        {
+                            playButton.pauseSimulation()
+                        }
+                        else
+                        {
+                            UM.SimulationView.setCurrentLayer(currentLayer+1)
+                            UM.SimulationView.setCurrentPath(0)
+                        }
+                    }
+                    else
+                    {
+                        UM.SimulationView.setCurrentPath(currentPath+1)
+                    }
+                }
+                playButton.status = 1
+            }
+        }
+    }
+
+    FontMetrics {
+        id: fontMetrics
+        font: UM.Theme.getFont("default")
+    }
+}

+ 259 - 0
plugins/SimulationView/SimulationViewProxy.py

@@ -0,0 +1,259 @@
+# Copyright (c) 2017 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty
+from UM.FlameProfiler import pyqtSlot
+from UM.Application import Application
+
+import SimulationView
+
+
+class SimulationViewProxy(QObject):
+    def __init__(self, parent=None):
+        super().__init__(parent)
+        self._current_layer = 0
+        self._controller = Application.getInstance().getController()
+        self._controller.activeViewChanged.connect(self._onActiveViewChanged)
+        self._onActiveViewChanged()
+        self.is_simulationView_selected = False
+
+    currentLayerChanged = pyqtSignal()
+    currentPathChanged = pyqtSignal()
+    maxLayersChanged = pyqtSignal()
+    maxPathsChanged = pyqtSignal()
+    activityChanged = pyqtSignal()
+    globalStackChanged = pyqtSignal()
+    preferencesChanged = pyqtSignal()
+    busyChanged = pyqtSignal()
+
+    @pyqtProperty(bool, notify=activityChanged)
+    def layerActivity(self):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            return active_view.getActivity()
+        return False
+
+    @pyqtProperty(int, notify=maxLayersChanged)
+    def numLayers(self):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            return active_view.getMaxLayers()
+        return 0
+
+    @pyqtProperty(int, notify=currentLayerChanged)
+    def currentLayer(self):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            return active_view.getCurrentLayer()
+        return 0
+
+    @pyqtProperty(int, notify=currentLayerChanged)
+    def minimumLayer(self):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            return active_view.getMinimumLayer()
+        return 0
+
+    @pyqtProperty(int, notify=maxPathsChanged)
+    def numPaths(self):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            return active_view.getMaxPaths()
+        return 0
+
+    @pyqtProperty(int, notify=currentPathChanged)
+    def currentPath(self):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            return active_view.getCurrentPath()
+        return 0
+
+    @pyqtProperty(int, notify=currentPathChanged)
+    def minimumPath(self):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            return active_view.getMinimumPath()
+        return 0
+
+    @pyqtProperty(bool, notify=busyChanged)
+    def busy(self):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            return active_view.isBusy()
+        return False
+
+    @pyqtProperty(bool, notify=preferencesChanged)
+    def compatibilityMode(self):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            return active_view.getCompatibilityMode()
+        return False
+
+    @pyqtSlot(int)
+    def setCurrentLayer(self, layer_num):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            active_view.setLayer(layer_num)
+
+    @pyqtSlot(int)
+    def setMinimumLayer(self, layer_num):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            active_view.setMinimumLayer(layer_num)
+
+    @pyqtSlot(int)
+    def setCurrentPath(self, path_num):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            active_view.setPath(path_num)
+
+    @pyqtSlot(int)
+    def setMinimumPath(self, path_num):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            active_view.setMinimumPath(path_num)
+
+    @pyqtSlot(int)
+    def setSimulationViewType(self, layer_view_type):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            active_view.setSimulationViewisinstance(layer_view_type)
+
+    @pyqtSlot(result=int)
+    def getSimulationViewType(self):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            return active_view.getSimulationViewType()
+        return 0
+
+    @pyqtSlot(bool)
+    def setSimulationRunning(self, running):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            active_view.setSimulationRunning(running)
+
+    @pyqtSlot(result=bool)
+    def getSimulationRunning(self):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            return active_view.isSimulationRunning()
+        return False
+
+    @pyqtSlot(result=float)
+    def getMinFeedrate(self):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            return active_view.getMinFeedrate()
+        return 0
+
+    @pyqtSlot(result=float)
+    def getMaxFeedrate(self):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            return active_view.getMaxFeedrate()
+        return 0
+
+    @pyqtSlot(result=float)
+    def getMinThickness(self):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            return active_view.getMinThickness()
+        return 0
+
+    @pyqtSlot(result=float)
+    def getMaxThickness(self):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            return active_view.getMaxThickness()
+        return 0
+
+    # Opacity 0..1
+    @pyqtSlot(int, float)
+    def setExtruderOpacity(self, extruder_nr, opacity):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            active_view.setExtruderOpacity(extruder_nr, opacity)
+
+    @pyqtSlot(int)
+    def setShowTravelMoves(self, show):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            active_view.setShowTravelMoves(show)
+
+    @pyqtSlot(int)
+    def setShowHelpers(self, show):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            active_view.setShowHelpers(show)
+
+    @pyqtSlot(int)
+    def setShowSkin(self, show):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            active_view.setShowSkin(show)
+
+    @pyqtSlot(int)
+    def setShowInfill(self, show):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            active_view.setShowInfill(show)
+
+    @pyqtProperty(int, notify=globalStackChanged)
+    def extruderCount(self):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            return active_view.getExtruderCount()
+        return 0
+
+    def _layerActivityChanged(self):
+        self.activityChanged.emit()
+
+    def _onLayerChanged(self):
+        self.currentLayerChanged.emit()
+        self._layerActivityChanged()
+
+    def _onPathChanged(self):
+        self.currentPathChanged.emit()
+        self._layerActivityChanged()
+
+    def _onMaxLayersChanged(self):
+        self.maxLayersChanged.emit()
+
+    def _onMaxPathsChanged(self):
+        self.maxPathsChanged.emit()
+
+    def _onBusyChanged(self):
+        self.busyChanged.emit()
+
+    def _onActivityChanged(self):
+        self.activityChanged.emit()
+
+    def _onGlobalStackChanged(self):
+        self.globalStackChanged.emit()
+
+    def _onPreferencesChanged(self):
+        self.preferencesChanged.emit()
+
+    def _onActiveViewChanged(self):
+        active_view = self._controller.getActiveView()
+        if isinstance(active_view, SimulationView.SimulationView.SimulationView):
+            # remove other connection if once the SimulationView was created.
+            if self.is_simulationView_selected:
+                active_view.currentLayerNumChanged.disconnect(self._onLayerChanged)
+                active_view.currentPathNumChanged.disconnect(self._onPathChanged)
+                active_view.maxLayersChanged.disconnect(self._onMaxLayersChanged)
+                active_view.maxPathsChanged.disconnect(self._onMaxPathsChanged)
+                active_view.busyChanged.disconnect(self._onBusyChanged)
+                active_view.activityChanged.disconnect(self._onActivityChanged)
+                active_view.globalStackChanged.disconnect(self._onGlobalStackChanged)
+                active_view.preferencesChanged.disconnect(self._onPreferencesChanged)
+
+            self.is_simulationView_selected = True
+            active_view.currentLayerNumChanged.connect(self._onLayerChanged)
+            active_view.currentPathNumChanged.connect(self._onPathChanged)
+            active_view.maxLayersChanged.connect(self._onMaxLayersChanged)
+            active_view.maxPathsChanged.connect(self._onMaxPathsChanged)
+            active_view.busyChanged.connect(self._onBusyChanged)
+            active_view.activityChanged.connect(self._onActivityChanged)
+            active_view.globalStackChanged.connect(self._onGlobalStackChanged)
+            active_view.preferencesChanged.connect(self._onPreferencesChanged)

+ 26 - 0
plugins/SimulationView/__init__.py

@@ -0,0 +1,26 @@
+# Copyright (c) 2017 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from PyQt5.QtQml import qmlRegisterSingletonType
+
+from UM.i18n import i18nCatalog
+from . import SimulationViewProxy, SimulationView
+
+catalog = i18nCatalog("cura")
+
+def getMetaData():
+    return {
+        "view": {
+            "name": catalog.i18nc("@item:inlistbox", "Simulation view"),
+            "view_panel": "SimulationView.qml",
+            "weight": 2
+        }
+    }
+
+def createSimulationViewProxy(engine, script_engine):
+    return SimulationViewProxy.SimulatorViewProxy()
+
+def register(app):
+    simulation_view = SimulationView.SimulationView()
+    qmlRegisterSingletonType(SimulationViewProxy.SimulationViewProxy, "UM", 1, 0, "SimulationView", simulation_view.getProxy)
+    return { "view": SimulationView.SimulationView()}

+ 156 - 0
plugins/SimulationView/layers.shader

@@ -0,0 +1,156 @@
+[shaders]
+vertex =
+    uniform highp mat4 u_modelViewProjectionMatrix;
+    uniform lowp float u_active_extruder;
+    uniform lowp float u_shade_factor;
+    uniform highp int u_layer_view_type;
+
+    attribute highp float a_extruder;
+    attribute highp float a_line_type;
+    attribute highp vec4 a_vertex;
+    attribute lowp vec4 a_color;
+    attribute lowp vec4 a_material_color;
+
+    varying lowp vec4 v_color;
+    varying float v_line_type;
+
+    void main()
+    {
+        gl_Position = u_modelViewProjectionMatrix * a_vertex;
+        // shade the color depending on the extruder index
+        v_color = a_color;
+        // 8 and 9 are travel moves
+        if ((a_line_type != 8.0) && (a_line_type != 9.0)) {
+            v_color = (a_extruder == u_active_extruder) ? v_color : vec4(u_shade_factor * v_color.rgb, v_color.a);
+        }
+
+        v_line_type = a_line_type;
+    }
+
+fragment =
+    varying lowp vec4 v_color;
+    varying float v_line_type;
+
+    uniform int u_show_travel_moves;
+    uniform int u_show_helpers;
+    uniform int u_show_skin;
+    uniform int u_show_infill;
+
+    void main()
+    {
+        if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5)) {  // actually, 8 and 9
+            // discard movements
+            discard;
+        }
+        // support: 4, 5, 7, 10
+        if ((u_show_helpers == 0) && (
+            ((v_line_type >= 3.5) && (v_line_type <= 4.5)) ||
+            ((v_line_type >= 6.5) && (v_line_type <= 7.5)) ||
+            ((v_line_type >= 9.5) && (v_line_type <= 10.5)) ||
+            ((v_line_type >= 4.5) && (v_line_type <= 5.5))
+            )) {
+            discard;
+        }
+        // skin: 1, 2, 3
+        if ((u_show_skin == 0) && (
+            (v_line_type >= 0.5) && (v_line_type <= 3.5)
+            )) {
+            discard;
+        }
+        // infill:
+        if ((u_show_infill == 0) && (v_line_type >= 5.5) && (v_line_type <= 6.5)) {
+            // discard movements
+            discard;
+        }
+
+        gl_FragColor = v_color;
+    }
+
+vertex41core =
+    #version 410
+    uniform highp mat4 u_modelViewProjectionMatrix;
+    uniform lowp float u_active_extruder;
+    uniform lowp float u_shade_factor;
+    uniform highp int u_layer_view_type;
+
+    in highp float a_extruder;
+    in highp float a_line_type;
+    in highp vec4 a_vertex;
+    in lowp vec4 a_color;
+    in lowp vec4 a_material_color;
+
+    out lowp vec4 v_color;
+    out float v_line_type;
+
+    void main()
+    {
+        gl_Position = u_modelViewProjectionMatrix * a_vertex;
+        v_color = a_color;
+        if ((a_line_type != 8) && (a_line_type != 9)) {
+            v_color = (a_extruder == u_active_extruder) ? v_color : vec4(u_shade_factor * v_color.rgb, v_color.a);
+        }
+
+        v_line_type = a_line_type;
+    }
+
+fragment41core =
+    #version 410
+    in lowp vec4 v_color;
+    in float v_line_type;
+    out vec4 frag_color;
+
+    uniform int u_show_travel_moves;
+    uniform int u_show_helpers;
+    uniform int u_show_skin;
+    uniform int u_show_infill;
+
+    void main()
+    {
+        if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5)) {  // actually, 8 and 9
+            // discard movements
+            discard;
+        }
+        // helpers: 4, 5, 7, 10
+        if ((u_show_helpers == 0) && (
+            ((v_line_type >= 3.5) && (v_line_type <= 4.5)) ||
+            ((v_line_type >= 6.5) && (v_line_type <= 7.5)) ||
+            ((v_line_type >= 9.5) && (v_line_type <= 10.5)) ||
+            ((v_line_type >= 4.5) && (v_line_type <= 5.5))
+            )) {
+            discard;
+        }
+        // skin: 1, 2, 3
+        if ((u_show_skin == 0) && (
+            (v_line_type >= 0.5) && (v_line_type <= 3.5)
+            )) {
+            discard;
+        }
+        // infill:
+        if ((u_show_infill == 0) && (v_line_type >= 5.5) && (v_line_type <= 6.5)) {
+            // discard movements
+            discard;
+        }
+
+        frag_color = v_color;
+    }
+
+[defaults]
+u_active_extruder = 0.0
+u_shade_factor = 0.60
+u_layer_view_type = 0
+u_extruder_opacity = [1.0, 1.0, 1.0, 1.0]
+
+u_show_travel_moves = 0
+u_show_helpers = 1
+u_show_skin = 1
+u_show_infill = 1
+
+[bindings]
+u_modelViewProjectionMatrix = model_view_projection_matrix
+
+[attributes]
+a_vertex = vertex
+a_color = color
+a_extruder = extruder
+a_line_type = line_type
+a_material_color = material_color

Some files were not shown because too many files changed in this diff