From 2df06bbbb9d517d66b7fb5b722e4eddc3b0afc47 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 21 Nov 2017 10:51:57 +0100 Subject: [PATCH] CURA-4526 Add Simulation View plugin --- plugins/SimulationView/LayerSlider.qml | 325 +++++++++ plugins/SimulationView/NozzleNode.py | 49 ++ plugins/SimulationView/PathSlider.qml | 161 +++++ plugins/SimulationView/SimulationPass.py | 186 +++++ .../SimulationView/SimulationSliderLabel.qml | 104 +++ plugins/SimulationView/SimulationView.py | 609 +++++++++++++++++ plugins/SimulationView/SimulationView.qml | 645 ++++++++++++++++++ plugins/SimulationView/SimulationViewProxy.py | 259 +++++++ plugins/SimulationView/__init__.py | 26 + plugins/SimulationView/layers.shader | 156 +++++ plugins/SimulationView/layers3d.shader | 293 ++++++++ plugins/SimulationView/layers3d_shadow.shader | 256 +++++++ plugins/SimulationView/layers_shadow.shader | 156 +++++ plugins/SimulationView/plugin.json | 8 + plugins/SimulationView/resources/nozzle.stl | Bin 0 -> 210284 bytes .../resources/simulation_pause.svg | 79 +++ .../resources/simulation_resume.svg | 82 +++ .../simulationview_composite.shader | 148 ++++ 18 files changed, 3542 insertions(+) create mode 100644 plugins/SimulationView/LayerSlider.qml create mode 100644 plugins/SimulationView/NozzleNode.py create mode 100644 plugins/SimulationView/PathSlider.qml create mode 100644 plugins/SimulationView/SimulationPass.py create mode 100644 plugins/SimulationView/SimulationSliderLabel.qml create mode 100644 plugins/SimulationView/SimulationView.py create mode 100644 plugins/SimulationView/SimulationView.qml create mode 100644 plugins/SimulationView/SimulationViewProxy.py create mode 100644 plugins/SimulationView/__init__.py create mode 100644 plugins/SimulationView/layers.shader create mode 100644 plugins/SimulationView/layers3d.shader create mode 100644 plugins/SimulationView/layers3d_shadow.shader create mode 100644 plugins/SimulationView/layers_shadow.shader create mode 100644 plugins/SimulationView/plugin.json create mode 100644 plugins/SimulationView/resources/nozzle.stl create mode 100644 plugins/SimulationView/resources/simulation_pause.svg create mode 100644 plugins/SimulationView/resources/simulation_resume.svg create mode 100644 plugins/SimulationView/simulationview_composite.shader diff --git a/plugins/SimulationView/LayerSlider.qml b/plugins/SimulationView/LayerSlider.qml new file mode 100644 index 0000000000..22f9d91340 --- /dev/null +++ b/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 + } + } +} diff --git a/plugins/SimulationView/NozzleNode.py b/plugins/SimulationView/NozzleNode.py new file mode 100644 index 0000000000..8a29871775 --- /dev/null +++ b/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 diff --git a/plugins/SimulationView/PathSlider.qml b/plugins/SimulationView/PathSlider.qml new file mode 100644 index 0000000000..0a4af904aa --- /dev/null +++ b/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 + } + } +} diff --git a/plugins/SimulationView/SimulationPass.py b/plugins/SimulationView/SimulationPass.py new file mode 100644 index 0000000000..24a13eaf7a --- /dev/null +++ b/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() diff --git a/plugins/SimulationView/SimulationSliderLabel.qml b/plugins/SimulationView/SimulationSliderLabel.qml new file mode 100644 index 0000000000..1c8daf867f --- /dev/null +++ b/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 + } +} diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py new file mode 100644 index 0000000000..2751ea4f60 --- /dev/null +++ b/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() + diff --git a/plugins/SimulationView/SimulationView.qml b/plugins/SimulationView/SimulationView.qml new file mode 100644 index 0000000000..4c7d99deec --- /dev/null +++ b/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") + } +} diff --git a/plugins/SimulationView/SimulationViewProxy.py b/plugins/SimulationView/SimulationViewProxy.py new file mode 100644 index 0000000000..e144b841e6 --- /dev/null +++ b/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) diff --git a/plugins/SimulationView/__init__.py b/plugins/SimulationView/__init__.py new file mode 100644 index 0000000000..f7ccf41acc --- /dev/null +++ b/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()} diff --git a/plugins/SimulationView/layers.shader b/plugins/SimulationView/layers.shader new file mode 100644 index 0000000000..d340773403 --- /dev/null +++ b/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 diff --git a/plugins/SimulationView/layers3d.shader b/plugins/SimulationView/layers3d.shader new file mode 100644 index 0000000000..f377fca055 --- /dev/null +++ b/plugins/SimulationView/layers3d.shader @@ -0,0 +1,293 @@ +[shaders] +vertex41core = + #version 410 + uniform highp mat4 u_modelViewProjectionMatrix; + + uniform highp mat4 u_modelMatrix; + uniform highp mat4 u_viewProjectionMatrix; + uniform lowp float u_active_extruder; + uniform lowp float u_max_feedrate; + uniform lowp float u_min_feedrate; + uniform lowp float u_max_thickness; + uniform lowp float u_min_thickness; + uniform lowp int u_layer_view_type; + uniform lowp vec4 u_extruder_opacity; // currently only for max 4 extruders, others always visible + + uniform highp mat4 u_normalMatrix; + + in highp vec4 a_vertex; + in lowp vec4 a_color; + in lowp vec4 a_material_color; + in highp vec4 a_normal; + in highp vec2 a_line_dim; // line width and thickness + in highp float a_extruder; + in highp float a_line_type; + in highp float a_feedrate; + in highp float a_thickness; + + out lowp vec4 v_color; + + out highp vec3 v_vertex; + out highp vec3 v_normal; + out lowp vec2 v_line_dim; + out highp int v_extruder; + out highp vec4 v_extruder_opacity; + out float v_line_type; + + out lowp vec4 f_color; + out highp vec3 f_vertex; + out highp vec3 f_normal; + + vec4 gradientColor(float abs_value, float min_value, float max_value) + { + float value = (abs_value - min_value)/(max_value - min_value); + float red = value; + float green = 1-abs(1-2*value); + float blue = 1-value; + return vec4(red, green, blue, 1.0); + } + + void main() + { + vec4 v1_vertex = a_vertex; + v1_vertex.y -= a_line_dim.y / 2; // half layer down + + vec4 world_space_vert = u_modelMatrix * v1_vertex; + gl_Position = world_space_vert; + // shade the color depending on the extruder index stored in the alpha component of the color + + switch (u_layer_view_type) { + case 0: // "Material color" + v_color = a_material_color; + break; + case 1: // "Line type" + v_color = a_color; + break; + case 2: // "Feedrate" + v_color = gradientColor(a_feedrate, u_min_feedrate, u_max_feedrate); + break; + case 3: // "Layer thickness" + v_color = gradientColor(a_line_dim.y, u_min_thickness, u_max_thickness); + break; + } + + v_vertex = world_space_vert.xyz; + v_normal = (u_normalMatrix * normalize(a_normal)).xyz; + v_line_dim = a_line_dim; + v_extruder = int(a_extruder); + v_line_type = a_line_type; + v_extruder_opacity = u_extruder_opacity; + + // for testing without geometry shader + f_color = v_color; + f_vertex = v_vertex; + f_normal = v_normal; + } + +geometry41core = + #version 410 + + uniform highp mat4 u_viewProjectionMatrix; + uniform int u_show_travel_moves; + uniform int u_show_helpers; + uniform int u_show_skin; + uniform int u_show_infill; + + layout(lines) in; + layout(triangle_strip, max_vertices = 26) out; + + in vec4 v_color[]; + in vec3 v_vertex[]; + in vec3 v_normal[]; + in vec2 v_line_dim[]; + in int v_extruder[]; + in vec4 v_extruder_opacity[]; + in float v_line_type[]; + + out vec4 f_color; + out vec3 f_normal; + out vec3 f_vertex; + + // Set the set of variables and EmitVertex + void myEmitVertex(vec3 vertex, vec4 color, vec3 normal, vec4 pos) { + f_vertex = vertex; + f_color = color; + f_normal = normal; + gl_Position = pos; + EmitVertex(); + } + + void main() + { + vec4 g_vertex_delta; + vec3 g_vertex_normal_horz; // horizontal and vertical in respect to layers + vec4 g_vertex_offset_horz; // vec4 to match gl_in[x].gl_Position + vec3 g_vertex_normal_vert; + vec4 g_vertex_offset_vert; + vec3 g_vertex_normal_horz_head; + vec4 g_vertex_offset_horz_head; + + float size_x; + float size_y; + + if ((v_extruder_opacity[0][v_extruder[0]] == 0.0) && (v_line_type[0] != 8) && (v_line_type[0] != 9)) { + return; + } + // See LayerPolygon; 8 is MoveCombingType, 9 is RetractionType + if ((u_show_travel_moves == 0) && ((v_line_type[0] == 8) || (v_line_type[0] == 9))) { + return; + } + if ((u_show_helpers == 0) && ((v_line_type[0] == 4) || (v_line_type[0] == 5) || (v_line_type[0] == 7) || (v_line_type[0] == 10))) { + return; + } + if ((u_show_skin == 0) && ((v_line_type[0] == 1) || (v_line_type[0] == 2) || (v_line_type[0] == 3))) { + return; + } + if ((u_show_infill == 0) && (v_line_type[0] == 6)) { + return; + } + + if ((v_line_type[0] == 8) || (v_line_type[0] == 9)) { + // fixed size for movements + size_x = 0.05; + } else { + size_x = v_line_dim[1].x / 2 + 0.01; // radius, and make it nicely overlapping + } + size_y = v_line_dim[1].y / 2 + 0.01; + + g_vertex_delta = gl_in[1].gl_Position - gl_in[0].gl_Position; + g_vertex_normal_horz_head = normalize(vec3(-g_vertex_delta.x, -g_vertex_delta.y, -g_vertex_delta.z)); + g_vertex_offset_horz_head = vec4(g_vertex_normal_horz_head * size_x, 0.0); + + g_vertex_normal_horz = normalize(vec3(g_vertex_delta.z, g_vertex_delta.y, -g_vertex_delta.x)); + + g_vertex_offset_horz = vec4(g_vertex_normal_horz * size_x, 0.0); //size * g_vertex_normal_horz; + g_vertex_normal_vert = vec3(0.0, 1.0, 0.0); + g_vertex_offset_vert = vec4(g_vertex_normal_vert * size_y, 0.0); + + if ((v_line_type[0] == 8) || (v_line_type[0] == 9)) { + // Travels: flat plane with pointy ends + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert)); + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head + g_vertex_offset_vert)); + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert)); + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert)); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert)); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert)); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head + g_vertex_offset_vert)); + + EndPrimitive(); + } else { + // All normal lines are rendered as 3d tubes. + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz)); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz)); + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert)); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert)); + myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz)); + myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz)); + myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert)); + myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert)); + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz)); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz)); + + EndPrimitive(); + + // left side + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz)); + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert)); + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head)); + myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz)); + + EndPrimitive(); + + myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz)); + myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert)); + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head)); + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz)); + + EndPrimitive(); + + // right side + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz)); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert)); + myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head)); + myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz)); + + EndPrimitive(); + + myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz)); + myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert)); + myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head)); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz)); + + EndPrimitive(); + } + } + +fragment41core = + #version 410 + in lowp vec4 f_color; + in lowp vec3 f_normal; + in lowp vec3 f_vertex; + + out vec4 frag_color; + + uniform mediump vec4 u_ambientColor; + uniform highp vec3 u_lightPosition; + + void main() + { + mediump vec4 finalColor = vec4(0.0); + float alpha = f_color.a; + + finalColor.rgb += f_color.rgb * 0.3; + + highp vec3 normal = normalize(f_normal); + highp vec3 light_dir = normalize(u_lightPosition - f_vertex); + + // Diffuse Component + highp float NdotL = clamp(dot(normal, light_dir), 0.0, 1.0); + finalColor += (NdotL * f_color); + finalColor.a = alpha; // Do not change alpha in any way + + frag_color = finalColor; + } + + +[defaults] +u_active_extruder = 0.0 +u_layer_view_type = 0 +u_extruder_opacity = [1.0, 1.0, 1.0, 1.0] + +u_specularColor = [0.4, 0.4, 0.4, 1.0] +u_ambientColor = [0.3, 0.3, 0.3, 0.0] +u_diffuseColor = [1.0, 0.79, 0.14, 1.0] +u_shininess = 20.0 + +u_show_travel_moves = 0 +u_show_helpers = 1 +u_show_skin = 1 +u_show_infill = 1 + +u_min_feedrate = 0 +u_max_feedrate = 1 + +u_min_thickness = 0 +u_max_thickness = 1 + +[bindings] +u_modelViewProjectionMatrix = model_view_projection_matrix +u_modelMatrix = model_matrix +u_viewProjectionMatrix = view_projection_matrix +u_normalMatrix = normal_matrix +u_lightPosition = light_0_position + +[attributes] +a_vertex = vertex +a_color = color +a_normal = normal +a_line_dim = line_dim +a_extruder = extruder +a_material_color = material_color +a_line_type = line_type +a_feedrate = feedrate +a_thickness = thickness diff --git a/plugins/SimulationView/layers3d_shadow.shader b/plugins/SimulationView/layers3d_shadow.shader new file mode 100644 index 0000000000..ad75fcf9d0 --- /dev/null +++ b/plugins/SimulationView/layers3d_shadow.shader @@ -0,0 +1,256 @@ +[shaders] +vertex41core = + #version 410 + uniform highp mat4 u_modelViewProjectionMatrix; + + uniform highp mat4 u_modelMatrix; + uniform highp mat4 u_viewProjectionMatrix; + uniform lowp float u_active_extruder; + uniform lowp vec4 u_extruder_opacity; // currently only for max 4 extruders, others always visible + + uniform highp mat4 u_normalMatrix; + + in highp vec4 a_vertex; + in lowp vec4 a_color; + in lowp vec4 a_grayColor; + in lowp vec4 a_material_color; + in highp vec4 a_normal; + in highp vec2 a_line_dim; // line width and thickness + in highp float a_extruder; + in highp float a_line_type; + + out lowp vec4 v_color; + + out highp vec3 v_vertex; + out highp vec3 v_normal; + out lowp vec2 v_line_dim; + out highp int v_extruder; + out highp vec4 v_extruder_opacity; + out float v_line_type; + + out lowp vec4 f_color; + out highp vec3 f_vertex; + out highp vec3 f_normal; + + void main() + { + vec4 v1_vertex = a_vertex; + v1_vertex.y -= a_line_dim.y / 2; // half layer down + + vec4 world_space_vert = u_modelMatrix * v1_vertex; + gl_Position = world_space_vert; + // shade the color depending on the extruder index stored in the alpha component of the color + + v_color = vec4(0.4, 0.4, 0.4, 0.9); // default color for not current layer + v_vertex = world_space_vert.xyz; + v_normal = (u_normalMatrix * normalize(a_normal)).xyz; + v_line_dim = a_line_dim; + v_extruder = int(a_extruder); + v_line_type = a_line_type; + v_extruder_opacity = u_extruder_opacity; + + // for testing without geometry shader + f_color = v_color; + f_vertex = v_vertex; + f_normal = v_normal; + } + +geometry41core = + #version 410 + + uniform highp mat4 u_viewProjectionMatrix; + uniform int u_show_travel_moves; + uniform int u_show_helpers; + uniform int u_show_skin; + uniform int u_show_infill; + + layout(lines) in; + layout(triangle_strip, max_vertices = 26) out; + + in vec4 v_color[]; + in vec3 v_vertex[]; + in vec3 v_normal[]; + in vec2 v_line_dim[]; + in int v_extruder[]; + in vec4 v_extruder_opacity[]; + in float v_line_type[]; + + out vec4 f_color; + out vec3 f_normal; + out vec3 f_vertex; + + // Set the set of variables and EmitVertex + void myEmitVertex(vec3 vertex, vec4 color, vec3 normal, vec4 pos) { + f_vertex = vertex; + f_color = color; + f_normal = normal; + gl_Position = pos; + EmitVertex(); + } + + void main() + { + vec4 g_vertex_delta; + vec3 g_vertex_normal_horz; // horizontal and vertical in respect to layers + vec4 g_vertex_offset_horz; // vec4 to match gl_in[x].gl_Position + vec3 g_vertex_normal_vert; + vec4 g_vertex_offset_vert; + vec3 g_vertex_normal_horz_head; + vec4 g_vertex_offset_horz_head; + + float size_x; + float size_y; + + if ((v_extruder_opacity[0][v_extruder[0]] == 0.0) && (v_line_type[0] != 8) && (v_line_type[0] != 9)) { + return; + } + // See LayerPolygon; 8 is MoveCombingType, 9 is RetractionType + if ((u_show_travel_moves == 0) && ((v_line_type[0] == 8) || (v_line_type[0] == 9))) { + return; + } + if ((u_show_helpers == 0) && ((v_line_type[0] == 4) || (v_line_type[0] == 5) || (v_line_type[0] == 7) || (v_line_type[0] == 10))) { + return; + } + if ((u_show_skin == 0) && ((v_line_type[0] == 1) || (v_line_type[0] == 2) || (v_line_type[0] == 3))) { + return; + } + if ((u_show_infill == 0) && (v_line_type[0] == 6)) { + return; + } + + if ((v_line_type[0] == 8) || (v_line_type[0] == 9)) { + // fixed size for movements + size_x = 0.05; + } else { + size_x = v_line_dim[1].x / 2 + 0.01; // radius, and make it nicely overlapping + } + size_y = v_line_dim[1].y / 2 + 0.01; + + g_vertex_delta = gl_in[1].gl_Position - gl_in[0].gl_Position; + g_vertex_normal_horz_head = normalize(vec3(-g_vertex_delta.x, -g_vertex_delta.y, -g_vertex_delta.z)); + g_vertex_offset_horz_head = vec4(g_vertex_normal_horz_head * size_x, 0.0); + + g_vertex_normal_horz = normalize(vec3(g_vertex_delta.z, g_vertex_delta.y, -g_vertex_delta.x)); + + g_vertex_offset_horz = vec4(g_vertex_normal_horz * size_x, 0.0); //size * g_vertex_normal_horz; + g_vertex_normal_vert = vec3(0.0, 1.0, 0.0); + g_vertex_offset_vert = vec4(g_vertex_normal_vert * size_y, 0.0); + + if ((v_line_type[0] == 8) || (v_line_type[0] == 9)) { + // Travels: flat plane with pointy ends + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert)); + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head + g_vertex_offset_vert)); + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert)); + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert)); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert)); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert)); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head + g_vertex_offset_vert)); + + EndPrimitive(); + } else { + // All normal lines are rendered as 3d tubes. + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz)); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz)); + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert)); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert)); + myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz)); + myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz)); + myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert)); + myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert)); + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz)); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz)); + + EndPrimitive(); + + // left side + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz)); + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert)); + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head)); + myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz)); + + EndPrimitive(); + + myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz)); + myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert)); + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head)); + myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz)); + + EndPrimitive(); + + // right side + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz)); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert)); + myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head)); + myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz)); + + EndPrimitive(); + + myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz)); + myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert)); + myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head)); + myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz)); + + EndPrimitive(); + } + } + +fragment41core = + #version 410 + in lowp vec4 f_color; + in lowp vec3 f_normal; + in lowp vec3 f_vertex; + + out vec4 frag_color; + + uniform mediump vec4 u_ambientColor; + uniform highp vec3 u_lightPosition; + + void main() + { + mediump vec4 finalColor = vec4(0.0); + float alpha = f_color.a; + + finalColor.rgb += f_color.rgb * 0.3; + + highp vec3 normal = normalize(f_normal); + highp vec3 light_dir = normalize(u_lightPosition - f_vertex); + + // Diffuse Component + highp float NdotL = clamp(dot(normal, light_dir), 0.0, 1.0); + finalColor += (NdotL * f_color); + finalColor.a = alpha; // Do not change alpha in any way + + frag_color = finalColor; + } + + +[defaults] +u_active_extruder = 0.0 +u_extruder_opacity = [1.0, 1.0, 1.0, 1.0] + +u_specularColor = [0.4, 0.4, 0.4, 1.0] +u_ambientColor = [0.3, 0.3, 0.3, 0.0] +u_diffuseColor = [1.0, 0.79, 0.14, 1.0] +u_shininess = 20.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 +u_modelMatrix = model_matrix +u_viewProjectionMatrix = view_projection_matrix +u_normalMatrix = normal_matrix +u_lightPosition = light_0_position + +[attributes] +a_vertex = vertex +a_color = color +a_grayColor = vec4(0.87, 0.12, 0.45, 1.0) +a_normal = normal +a_line_dim = line_dim +a_extruder = extruder +a_material_color = material_color +a_line_type = line_type diff --git a/plugins/SimulationView/layers_shadow.shader b/plugins/SimulationView/layers_shadow.shader new file mode 100644 index 0000000000..972f18c921 --- /dev/null +++ b/plugins/SimulationView/layers_shadow.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 = vec4(0.4, 0.4, 0.4, 0.9); // default color for not current layer; + // 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 = vec4(0.4, 0.4, 0.4, 0.9); // default color for not current layer + // 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 diff --git a/plugins/SimulationView/plugin.json b/plugins/SimulationView/plugin.json new file mode 100644 index 0000000000..0e7bec0626 --- /dev/null +++ b/plugins/SimulationView/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "Simulation View", + "author": "Ultimaker B.V.", + "version": "1.0.0", + "description": "Provides the Simulation view.", + "api": 4, + "i18n-catalog": "cura" +} diff --git a/plugins/SimulationView/resources/nozzle.stl b/plugins/SimulationView/resources/nozzle.stl new file mode 100644 index 0000000000000000000000000000000000000000..7f4b22804aab6768ed2b9c0ccdc2f89847e38125 GIT binary patch literal 210284 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%0AbuZy#5DmdXLV_bm} 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 MV-kv + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/plugins/SimulationView/resources/simulation_resume.svg b/plugins/SimulationView/resources/simulation_resume.svg new file mode 100644 index 0000000000..a8ed8e79a3 --- /dev/null +++ b/plugins/SimulationView/resources/simulation_resume.svg @@ -0,0 +1,82 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/plugins/SimulationView/simulationview_composite.shader b/plugins/SimulationView/simulationview_composite.shader new file mode 100644 index 0000000000..dcc02acc84 --- /dev/null +++ b/plugins/SimulationView/simulationview_composite.shader @@ -0,0 +1,148 @@ +[shaders] +vertex = + uniform highp mat4 u_modelViewProjectionMatrix; + attribute highp vec4 a_vertex; + attribute highp vec2 a_uvs; + + varying highp vec2 v_uvs; + + void main() + { + gl_Position = u_modelViewProjectionMatrix * a_vertex; + v_uvs = a_uvs; + } + +fragment = + uniform sampler2D u_layer0; + uniform sampler2D u_layer1; + uniform sampler2D u_layer2; + + uniform vec2 u_offset[9]; + + uniform vec4 u_background_color; + uniform float u_outline_strength; + uniform vec4 u_outline_color; + + varying vec2 v_uvs; + + float kernel[9]; + + const vec3 x_axis = vec3(1.0, 0.0, 0.0); + const vec3 y_axis = vec3(0.0, 1.0, 0.0); + const vec3 z_axis = vec3(0.0, 0.0, 1.0); + + void main() + { + // blur kernel + kernel[0] = 0.0; kernel[1] = 1.0; kernel[2] = 0.0; + kernel[3] = 1.0; kernel[4] = -4.0; kernel[5] = 1.0; + kernel[6] = 0.0; kernel[7] = 1.0; kernel[8] = 0.0; + + vec4 result = u_background_color; + + vec4 main_layer = texture2D(u_layer0, v_uvs); + vec4 selection_layer = texture2D(u_layer1, v_uvs); + vec4 layerview_layer = texture2D(u_layer2, v_uvs); + + result = main_layer * main_layer.a + result * (1.0 - main_layer.a); + result = layerview_layer * layerview_layer.a + result * (1.0 - layerview_layer.a); + + vec4 sum = vec4(0.0); + for (int i = 0; i < 9; i++) + { + vec4 color = vec4(texture2D(u_layer1, v_uvs.xy + u_offset[i]).a); + sum += color * (kernel[i] / u_outline_strength); + } + + if((selection_layer.rgb == x_axis || selection_layer.rgb == y_axis || selection_layer.rgb == z_axis)) + { + gl_FragColor = result; + } + else + { + gl_FragColor = mix(result, u_outline_color, abs(sum.a)); + } + } + +vertex41core = + #version 410 + uniform highp mat4 u_modelViewProjectionMatrix; + in highp vec4 a_vertex; + in highp vec2 a_uvs; + + out highp vec2 v_uvs; + + void main() + { + gl_Position = u_modelViewProjectionMatrix * a_vertex; + v_uvs = a_uvs; + } + +fragment41core = + #version 410 + uniform sampler2D u_layer0; + uniform sampler2D u_layer1; + uniform sampler2D u_layer2; + + uniform vec2 u_offset[9]; + + uniform vec4 u_background_color; + uniform float u_outline_strength; + uniform vec4 u_outline_color; + + in vec2 v_uvs; + + float kernel[9]; + + const vec3 x_axis = vec3(1.0, 0.0, 0.0); + const vec3 y_axis = vec3(0.0, 1.0, 0.0); + const vec3 z_axis = vec3(0.0, 0.0, 1.0); + + out vec4 frag_color; + + void main() + { + // blur kernel + kernel[0] = 0.0; kernel[1] = 1.0; kernel[2] = 0.0; + kernel[3] = 1.0; kernel[4] = -4.0; kernel[5] = 1.0; + kernel[6] = 0.0; kernel[7] = 1.0; kernel[8] = 0.0; + + vec4 result = u_background_color; + + vec4 main_layer = texture(u_layer0, v_uvs); + vec4 selection_layer = texture(u_layer1, v_uvs); + vec4 layerview_layer = texture(u_layer2, v_uvs); + + result = main_layer * main_layer.a + result * (1.0 - main_layer.a); + result = layerview_layer * layerview_layer.a + result * (1.0 - layerview_layer.a); + + vec4 sum = vec4(0.0); + for (int i = 0; i < 9; i++) + { + vec4 color = vec4(texture(u_layer1, v_uvs.xy + u_offset[i]).a); + sum += color * (kernel[i] / u_outline_strength); + } + + if((selection_layer.rgb == x_axis || selection_layer.rgb == y_axis || selection_layer.rgb == z_axis)) + { + frag_color = result; + } + else + { + frag_color = mix(result, u_outline_color, abs(sum.a)); + } + } + +[defaults] +u_layer0 = 0 +u_layer1 = 1 +u_layer2 = 2 +u_background_color = [0.965, 0.965, 0.965, 1.0] +u_outline_strength = 1.0 +u_outline_color = [0.05, 0.66, 0.89, 1.0] + +[bindings] + +[attributes] +a_vertex = vertex +a_uvs = uv