From b6e997c88d5d8ac846f550b2e2ef1a6efeb7a288 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 21 Nov 2017 10:47:29 +0100 Subject: [PATCH 1/3] CURA-4526 Delete LayerView plugin because it will be replaced with the SimulationView. This commit also adapts the code in order to accept the messages coming from the engine, with information about feedrates and line thicknesses. Add also some changes in the GCodeReader that reads feedrates and line thickness from the gcode file. --- cura/CuraApplication.py | 4 +- cura/Layer.py | 4 +- cura/LayerDataBuilder.py | 12 +- cura/LayerPolygon.py | 20 +- plugins/CuraEngineBackend/Cura.proto | 4 + .../CuraEngineBackend/CuraEngineBackend.py | 2 +- .../ProcessSlicedLayersJob.py | 18 +- plugins/GCodeReader/GCodeReader.py | 91 +++- plugins/LayerView/LayerPass.py | 113 ---- plugins/LayerView/LayerSlider.qml | 312 ----------- plugins/LayerView/LayerSliderLabel.qml | 103 ---- plugins/LayerView/LayerView.py | 495 ------------------ plugins/LayerView/LayerView.qml | 388 -------------- plugins/LayerView/LayerViewProxy.py | 151 ------ plugins/LayerView/__init__.py | 25 - plugins/LayerView/layers.shader | 156 ------ plugins/LayerView/layers3d.shader | 264 ---------- plugins/LayerView/layerview_composite.shader | 148 ------ plugins/LayerView/plugin.json | 8 - resources/themes/cura-dark/theme.json | 2 + resources/themes/cura-light/styles.qml | 1 + resources/themes/cura-light/theme.json | 4 +- 22 files changed, 116 insertions(+), 2209 deletions(-) delete mode 100755 plugins/LayerView/LayerPass.py delete mode 100644 plugins/LayerView/LayerSlider.qml delete mode 100644 plugins/LayerView/LayerSliderLabel.qml delete mode 100755 plugins/LayerView/LayerView.py delete mode 100755 plugins/LayerView/LayerView.qml delete mode 100644 plugins/LayerView/LayerViewProxy.py delete mode 100644 plugins/LayerView/__init__.py delete mode 100755 plugins/LayerView/layers.shader delete mode 100755 plugins/LayerView/layers3d.shader delete mode 100644 plugins/LayerView/layerview_composite.shader delete mode 100644 plugins/LayerView/plugin.json diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 8c1ee8fc36..caa39cc703 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -218,7 +218,7 @@ class CuraApplication(QtApplication): "CuraEngineBackend", "UserAgreement", "SolidView", - "LayerView", + "SimulationView", "STLReader", "SelectionTool", "CameraTool", @@ -1386,7 +1386,7 @@ class CuraApplication(QtApplication): extension = os.path.splitext(filename)[1] if extension.lower() in self._non_sliceable_extensions: - self.getController().setActiveView("LayerView") + self.getController().setActiveView("SimulationView") view = self.getController().getActiveView() view.resetLayerData() view.setLayer(9999999) diff --git a/cura/Layer.py b/cura/Layer.py index d5ef5c9bb4..9cd45380fc 100644 --- a/cura/Layer.py +++ b/cura/Layer.py @@ -47,12 +47,12 @@ class Layer: return result - def build(self, vertex_offset, index_offset, vertices, colors, line_dimensions, extruders, line_types, indices): + def build(self, vertex_offset, index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices): result_vertex_offset = vertex_offset result_index_offset = index_offset self._element_count = 0 for polygon in self._polygons: - polygon.build(result_vertex_offset, result_index_offset, vertices, colors, line_dimensions, extruders, line_types, indices) + polygon.build(result_vertex_offset, result_index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices) result_vertex_offset += polygon.lineMeshVertexCount() result_index_offset += polygon.lineMeshElementCount() self._element_count += polygon.elementCount diff --git a/cura/LayerDataBuilder.py b/cura/LayerDataBuilder.py index 6e50611e64..d6cc81a4e9 100755 --- a/cura/LayerDataBuilder.py +++ b/cura/LayerDataBuilder.py @@ -20,11 +20,11 @@ class LayerDataBuilder(MeshBuilder): if layer not in self._layers: self._layers[layer] = Layer(layer) - def addPolygon(self, layer, polygon_type, data, line_width): + def addPolygon(self, layer, polygon_type, data, line_width, line_thickness, line_feedrate): if layer not in self._layers: self.addLayer(layer) - p = LayerPolygon(self, polygon_type, data, line_width) + p = LayerPolygon(self, polygon_type, data, line_width, line_thickness, line_feedrate) self._layers[layer].polygons.append(p) def getLayer(self, layer): @@ -64,13 +64,14 @@ class LayerDataBuilder(MeshBuilder): line_dimensions = numpy.empty((vertex_count, 2), numpy.float32) colors = numpy.empty((vertex_count, 4), numpy.float32) indices = numpy.empty((index_count, 2), numpy.int32) + feedrates = numpy.empty((vertex_count), numpy.float32) extruders = numpy.empty((vertex_count), numpy.float32) line_types = numpy.empty((vertex_count), numpy.float32) vertex_offset = 0 index_offset = 0 for layer, data in sorted(self._layers.items()): - ( vertex_offset, index_offset ) = data.build( vertex_offset, index_offset, vertices, colors, line_dimensions, extruders, line_types, indices) + ( vertex_offset, index_offset ) = data.build( vertex_offset, index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices) self._element_counts[layer] = data.elementCount self.addVertices(vertices) @@ -107,6 +108,11 @@ class LayerDataBuilder(MeshBuilder): "value": line_types, "opengl_name": "a_line_type", "opengl_type": "float" + }, + "feedrates": { + "value": feedrates, + "opengl_name": "a_feedrate", + "opengl_type": "float" } } diff --git a/cura/LayerPolygon.py b/cura/LayerPolygon.py index 7f41351b7f..9766e0c82a 100644 --- a/cura/LayerPolygon.py +++ b/cura/LayerPolygon.py @@ -28,7 +28,8 @@ class LayerPolygon: # \param data new_points # \param line_widths array with line widths # \param line_thicknesses: array with type as index and thickness as value - def __init__(self, extruder, line_types, data, line_widths, line_thicknesses): + # \param line_feedrates array with line feedrates + def __init__(self, extruder, line_types, data, line_widths, line_thicknesses, line_feedrates): self._extruder = extruder self._types = line_types for i in range(len(self._types)): @@ -37,6 +38,7 @@ class LayerPolygon: self._data = data self._line_widths = line_widths self._line_thicknesses = line_thicknesses + self._line_feedrates = line_feedrates self._vertex_begin = 0 self._vertex_end = 0 @@ -84,10 +86,11 @@ class LayerPolygon: # \param vertices : vertex numpy array to be filled # \param colors : vertex numpy array to be filled # \param line_dimensions : vertex numpy array to be filled + # \param feedrates : vertex numpy array to be filled # \param extruders : vertex numpy array to be filled # \param line_types : vertex numpy array to be filled # \param indices : index numpy array to be filled - def build(self, vertex_offset, index_offset, vertices, colors, line_dimensions, extruders, line_types, indices): + def build(self, vertex_offset, index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices): if self._build_cache_line_mesh_mask is None or self._build_cache_needed_points is None: self.buildCache() @@ -109,10 +112,13 @@ class LayerPolygon: # Create an array with colors for each vertex and remove the color data for the points that has been thrown away. colors[self._vertex_begin:self._vertex_end, :] = numpy.tile(self._colors, (1, 2)).reshape((-1, 4))[needed_points_list.ravel()] - # Create an array with line widths for each vertex. + # Create an array with line widths and thicknesses for each vertex. line_dimensions[self._vertex_begin:self._vertex_end, 0] = numpy.tile(self._line_widths, (1, 2)).reshape((-1, 1))[needed_points_list.ravel()][:, 0] line_dimensions[self._vertex_begin:self._vertex_end, 1] = numpy.tile(self._line_thicknesses, (1, 2)).reshape((-1, 1))[needed_points_list.ravel()][:, 0] + # Create an array with feedrates for each line + feedrates[self._vertex_begin:self._vertex_end] = numpy.tile(self._line_feedrates, (1, 2)).reshape((-1, 1))[needed_points_list.ravel()][:, 0] + extruders[self._vertex_begin:self._vertex_end] = self._extruder # Convert type per vertex to type per line @@ -166,6 +172,14 @@ class LayerPolygon: @property def lineWidths(self): return self._line_widths + + @property + def lineThicknesses(self): + return self._line_thicknesses + + @property + def lineFeedrates(self): + return self._line_feedrates @property def jumpMask(self): diff --git a/plugins/CuraEngineBackend/Cura.proto b/plugins/CuraEngineBackend/Cura.proto index c2e4e5bb5f..69612210ec 100644 --- a/plugins/CuraEngineBackend/Cura.proto +++ b/plugins/CuraEngineBackend/Cura.proto @@ -61,6 +61,8 @@ message Polygon { Type type = 1; // Type of move bytes points = 2; // The points of the polygon, or two points if only a line segment (Currently only line segments are used) float line_width = 3; // The width of the line being laid down + float line_thickness = 4; // The thickness of the line being laid down + float line_feedrate = 5; // The feedrate of the line being laid down } message LayerOptimized { @@ -82,6 +84,8 @@ message PathSegment { bytes points = 3; // The points defining the line segments, bytes of float[2/3] array of length N+1 bytes line_type = 4; // Type of line segment as an unsigned char array of length 1 or N, where N is the number of line segments in this path bytes line_width = 5; // The widths of the line segments as bytes of a float array of length 1 or N + bytes line_thickness = 6; // The thickness of the line segments as bytes of a float array of length 1 or N + bytes line_feedrate = 7; // The feedrate of the line segments as bytes of a float array of length 1 or N } diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 14c1c10b90..d35df967b2 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -608,7 +608,7 @@ class CuraEngineBackend(QObject, Backend): def _onActiveViewChanged(self): if Application.getInstance().getController().getActiveView(): view = Application.getInstance().getController().getActiveView() - if view.getPluginId() == "LayerView": # If switching to layer view, we should process the layers if that hasn't been done yet. + if view.getPluginId() == "SimulationView": # If switching to layer view, we should process the layers if that hasn't been done yet. self._layer_view_active = True # There is data and we're not slicing at the moment # if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment. diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index a352564bc2..14646cbac1 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -61,7 +61,7 @@ class ProcessSlicedLayersJob(Job): def run(self): start_time = time() - if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": + if Application.getInstance().getController().getActiveView().getPluginId() == "SimulationView": self._progress_message.show() Job.yieldThread() if self._abort_requested: @@ -109,6 +109,7 @@ class ProcessSlicedLayersJob(Job): layer_data.addLayer(abs_layer_number) this_layer = layer_data.getLayer(abs_layer_number) layer_data.setLayerHeight(abs_layer_number, layer.height) + layer_data.setLayerThickness(abs_layer_number, layer.thickness) for p in range(layer.repeatedMessageCount("path_segment")): polygon = layer.getRepeatedMessage("path_segment", p) @@ -127,10 +128,11 @@ class ProcessSlicedLayersJob(Job): line_widths = numpy.fromstring(polygon.line_width, dtype="f4") # Convert bytearray to numpy array line_widths = line_widths.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. - # In the future, line_thicknesses should be given by CuraEngine as well. - # Currently the infill layer thickness also translates to line width - line_thicknesses = numpy.zeros(line_widths.shape, dtype="f4") - line_thicknesses[:] = layer.thickness / 1000 # from micrometer to millimeter + line_thicknesses = numpy.fromstring(polygon.line_thickness, dtype="f4") # Convert bytearray to numpy array + line_thicknesses = line_thicknesses.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. + + line_feedrates = numpy.fromstring(polygon.line_feedrate, dtype="f4") # Convert bytearray to numpy array + line_feedrates = line_feedrates.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. # Create a new 3D-array, copy the 2D points over and insert the right height. # This uses manual array creation + copy rather than numpy.insert since this is @@ -145,7 +147,7 @@ class ProcessSlicedLayersJob(Job): new_points[:, 1] = points[:, 2] new_points[:, 2] = -points[:, 1] - this_poly = LayerPolygon.LayerPolygon(extruder, line_types, new_points, line_widths, line_thicknesses) + this_poly = LayerPolygon.LayerPolygon(extruder, line_types, new_points, line_widths, line_thicknesses, line_feedrates) this_poly.buildCache() this_layer.polygons.append(this_poly) @@ -219,7 +221,7 @@ class ProcessSlicedLayersJob(Job): self._progress_message.setProgress(100) view = Application.getInstance().getController().getActiveView() - if view.getPluginId() == "LayerView": + if view.getPluginId() == "SimulationView": view.resetLayerData() if self._progress_message: @@ -232,7 +234,7 @@ class ProcessSlicedLayersJob(Job): def _onActiveViewChanged(self): if self.isRunning(): - if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": + if Application.getInstance().getController().getActiveView().getPluginId() == "SimulationView": if not self._progress_message: self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0, catalog.i18nc("@info:title", "Information")) if self._progress_message.getProgress() != 100: diff --git a/plugins/GCodeReader/GCodeReader.py b/plugins/GCodeReader/GCodeReader.py index 1b2795800e..2a7e29e370 100755 --- a/plugins/GCodeReader/GCodeReader.py +++ b/plugins/GCodeReader/GCodeReader.py @@ -1,4 +1,4 @@ -# Copyright (c) 2016 Aleph Objects, Inc. +# Copyright (c) 2017 Aleph Objects, Inc. # Cura is released under the terms of the LGPLv3 or higher. from UM.Application import Application @@ -40,7 +40,8 @@ class GCodeReader(MeshReader): self._extruder_number = 0 self._clearValues() self._scene_node = None - self._position = namedtuple('Position', ['x', 'y', 'z', 'e']) + # X, Y, Z position, F feedrate and E extruder values are stored + self._position = namedtuple('Position', ['x', 'y', 'z', 'f', 'e']) self._is_layers_in_file = False # Does the Gcode have the layers comment? self._extruder_offsets = {} # Offsets for multi extruders. key is index, value is [x-offset, y-offset] self._current_layer_thickness = 0.2 # default @@ -48,7 +49,9 @@ class GCodeReader(MeshReader): Preferences.getInstance().addPreference("gcodereader/show_caution", True) def _clearValues(self): + self._filament_diameter = 2.85 self._extruder_number = 0 + self._extrusion_length_offset = [0] self._layer_type = LayerPolygon.Inset0Type self._layer_number = 0 self._previous_z = 0 @@ -97,7 +100,7 @@ class GCodeReader(MeshReader): def _createPolygon(self, layer_thickness, path, extruder_offsets): countvalid = 0 for point in path: - if point[3] > 0: + if point[5] > 0: countvalid += 1 if countvalid >= 2: # we know what to do now, no need to count further @@ -115,27 +118,56 @@ class GCodeReader(MeshReader): line_types = numpy.empty((count - 1, 1), numpy.int32) line_widths = numpy.empty((count - 1, 1), numpy.float32) line_thicknesses = numpy.empty((count - 1, 1), numpy.float32) - # TODO: need to calculate actual line width based on E values + line_feedrates = numpy.empty((count - 1, 1), numpy.float32) line_widths[:, 0] = 0.35 # Just a guess line_thicknesses[:, 0] = layer_thickness points = numpy.empty((count, 3), numpy.float32) + extrusion_values = numpy.empty((count, 1), numpy.float32) i = 0 for point in path: points[i, :] = [point[0] + extruder_offsets[0], point[2], -point[1] - extruder_offsets[1]] + extrusion_values[i] = point[4] if i > 0: - line_types[i - 1] = point[3] - if point[3] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]: + line_feedrates[i - 1] = point[3] + line_types[i - 1] = point[5] + if point[5] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]: line_widths[i - 1] = 0.1 + line_thicknesses[i - 1] = 0.0 # Travels are set as zero thickness lines + else: + line_widths[i - 1] = self._calculateLineWidth(points[i], points[i-1], extrusion_values[i], extrusion_values[i-1], layer_thickness) i += 1 - this_poly = LayerPolygon(self._extruder_number, line_types, points, line_widths, line_thicknesses) + this_poly = LayerPolygon(self._extruder_number, line_types, points, line_widths, line_thicknesses, line_feedrates) this_poly.buildCache() this_layer.polygons.append(this_poly) return True + def _calculateLineWidth(self, current_point, previous_point, current_extrusion, previous_extrusion, layer_thickness): + # Area of the filament + Af = (self._filament_diameter / 2) ** 2 * numpy.pi + # Length of the extruded filament + de = current_extrusion - previous_extrusion + # Volumne of the extruded filament + dVe = de * Af + # Length of the printed line + dX = numpy.sqrt((current_point[0] - previous_point[0])**2 + (current_point[2] - previous_point[2])**2) + # When the extruder recovers from a retraction, we get zero distance + if dX == 0: + return 0.1 + # Area of the printed line. This area is a rectangle + Ae = dVe / dX + # This area is a rectangle with area equal to layer_thickness * layer_width + line_width = Ae / layer_thickness + + # A threshold is set to avoid weird paths in the GCode + if line_width > 1.2: + return 0.35 + return line_width + def _gCode0(self, position, params, path): - x, y, z, e = position + x, y, z, f, e = position + if self._is_absolute_positioning: x = params.x if params.x is not None else x y = params.y if params.y is not None else y @@ -145,22 +177,24 @@ class GCodeReader(MeshReader): y += params.y if params.y is not None else 0 z += params.z if params.z is not None else 0 + f = params.f if params.f is not None else f + if params.e is not None: new_extrusion_value = params.e if self._is_absolute_positioning else e[self._extruder_number] + params.e if new_extrusion_value > e[self._extruder_number]: - path.append([x, y, z, self._layer_type]) # extrusion + path.append([x, y, z, f, params.e + self._extrusion_length_offset[self._extruder_number], self._layer_type]) # extrusion else: - path.append([x, y, z, LayerPolygon.MoveRetractionType]) # retraction + path.append([x, y, z, f, params.e + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) # retraction e[self._extruder_number] = new_extrusion_value # Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions # Also, 1.5 is a heuristic for any priming or whatsoever, we skip those. if z > self._previous_z and (z - self._previous_z < 1.5): - self._current_layer_thickness = z - self._previous_z + 0.05 # allow a tiny overlap + self._current_layer_thickness = z - self._previous_z # allow a tiny overlap self._previous_z = z else: - path.append([x, y, z, LayerPolygon.MoveCombingType]) - return self._position(x, y, z, e) + path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType]) + return self._position(x, y, z, f, e) # G0 and G1 should be handled exactly the same. _gCode1 = _gCode0 @@ -171,6 +205,7 @@ class GCodeReader(MeshReader): params.x if params.x is not None else position.x, params.y if params.y is not None else position.y, 0, + position.f, position.e) ## Set the absolute positioning @@ -187,11 +222,14 @@ class GCodeReader(MeshReader): # For example: G92 X10 will set the X to 10 without any physical motion. def _gCode92(self, position, params, path): if params.e is not None: + # Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width + self._extrusion_length_offset[self._extruder_number] += position.e[self._extruder_number] - params.e position.e[self._extruder_number] = params.e return self._position( params.x if params.x is not None else position.x, params.y if params.y is not None else position.y, params.z if params.z is not None else position.z, + params.f if params.f is not None else position.f, position.e) def _processGCode(self, G, line, position, path): @@ -199,7 +237,7 @@ class GCodeReader(MeshReader): line = line.split(";", 1)[0] # Remove comments (if any) if func is not None: s = line.upper().split(" ") - x, y, z, e = None, None, None, None + x, y, z, f, e = None, None, None, None, None for item in s[1:]: if len(item) <= 1: continue @@ -211,17 +249,20 @@ class GCodeReader(MeshReader): y = float(item[1:]) if item[0] == "Z": z = float(item[1:]) + if item[0] == "F": + f = float(item[1:]) / 60 if item[0] == "E": e = float(item[1:]) if self._is_absolute_positioning and ((x is not None and x < 0) or (y is not None and y < 0)): self._center_is_zero = True - params = self._position(x, y, z, e) + params = self._position(x, y, z, f, e) return func(position, params, path) return position def _processTCode(self, T, line, position, path): self._extruder_number = T if self._extruder_number + 1 > len(position.e): + self._extrusion_length_offset.extend([0] * (self._extruder_number - len(position.e) + 1)) position.e.extend([0] * (self._extruder_number - len(position.e) + 1)) return position @@ -240,6 +281,8 @@ class GCodeReader(MeshReader): def read(self, file_name): Logger.log("d", "Preparing to load %s" % file_name) self._cancelled = False + # We obtain the filament diameter from the selected printer to calculate line widths + self._filament_diameter = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value") scene_node = SceneNode() # Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no @@ -277,7 +320,7 @@ class GCodeReader(MeshReader): Logger.log("d", "Parsing %s..." % file_name) - current_position = self._position(0, 0, 0, [0]) + current_position = self._position(0, 0, 0, 0, [0]) current_path = [] for line in file: @@ -310,6 +353,7 @@ class GCodeReader(MeshReader): else: Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type) + # When the layer change is reached, the polygon is computed so we have just one layer per layer per extruder if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword: try: layer_number = int(line[len(self._layer_keyword):]) @@ -325,17 +369,12 @@ class GCodeReader(MeshReader): G = self._getInt(line, "G") if G is not None: + # When find a movement, the new posistion is calculated and added to the current_path, but + # don't need to create a polygon until the end of the layer current_position = self._processGCode(G, line, current_position, current_path) - - # < 2 is a heuristic for a movement only, that should not be counted as a layer - if current_position.z > last_z and abs(current_position.z - last_z) < 2: - if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])): - current_path.clear() - if not self._is_layers_in_file: - self._layer_number += 1 - continue + # When changing the extruder, the polygon with the stored paths is computed if line.startswith("T"): T = self._getInt(line, "T") if T is not None: @@ -344,8 +383,8 @@ class GCodeReader(MeshReader): current_position = self._processTCode(T, line, current_position, current_path) - # "Flush" leftovers - if not self._is_layers_in_file and len(current_path) > 1: + # "Flush" leftovers. Last layer paths are still stored + if len(current_path) > 1: if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])): self._layer_number += 1 current_path.clear() diff --git a/plugins/LayerView/LayerPass.py b/plugins/LayerView/LayerPass.py deleted file mode 100755 index 963c8c75c8..0000000000 --- a/plugins/LayerView/LayerPass.py +++ /dev/null @@ -1,113 +0,0 @@ -# Copyright (c) 2016 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -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. -class LayerPass(RenderPass): - def __init__(self, width, height): - super().__init__("layerview", width, height) - - self._layer_shader = None - self._tool_handle_shader = None - 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 setLayerView(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" - else: - shader_filename = "layers3d.shader" - self._layer_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("LayerView"), shader_filename)) - # 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_layer_view_type", self._layer_view.getLayerViewType()) - 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_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")) - - self.bind() - - tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Overlay) - - for node in DepthFirstIterator(self._scene.getRoot()): - - if isinstance(node, ToolHandle): - tool_handle_batch.addItem(node.getWorldTransformation(), mesh = node.getSolidMesh()) - - 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()): - if layer > self._layer_view._current_layer_num: - break - if self._layer_view._minimum_layer_num > layer: - start += element_counts[layer] - end += element_counts[layer] - - # This uses glDrawRangeElements internally to only draw a certain range of lines. - batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid, mode = RenderBatch.RenderMode.Lines, range = (start, end)) - batch.addItem(node.getWorldTransformation(), layer_data) - batch.render(self._scene.getActiveCamera()) - - # 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()) - - # 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/LayerView/LayerSlider.qml b/plugins/LayerView/LayerSlider.qml deleted file mode 100644 index 9abeb01148..0000000000 --- a/plugins/LayerView/LayerSlider.qml +++ /dev/null @@ -1,312 +0,0 @@ -// 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 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.LayerView.setCurrentLayer(upperValue) - UM.LayerView.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.LayerView.setCurrentLayer(value) - UM.LayerView.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) - } - - LayerSliderLabel { - 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.LayerView.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: 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.LayerView.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.LayerView.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() - } - - // 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) - } - - LayerSliderLabel { - 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.LayerView.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: sliderRoot.lowerHandleColor - - visible: slider.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.LayerView.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.LayerView.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() - } - - // 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) - } - - LayerSliderLabel { - 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.LayerView.busy - setValue: lowerHandle.setValue // connect callback functions - } - } -} diff --git a/plugins/LayerView/LayerSliderLabel.qml b/plugins/LayerView/LayerSliderLabel.qml deleted file mode 100644 index c989679285..0000000000 --- a/plugins/LayerView/LayerSliderLabel.qml +++ /dev/null @@ -1,103 +0,0 @@ -// 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 - - 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 + 1 // 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 != "") { - // -1 because we need to convert back to an array structure - sliderLabelRoot.setValue(parseInt(valueLabel.text) - 1) - } - } - - validator: IntValidator { - bottom: 1 - top: sliderLabelRoot.maximumValue + 1 // +1 because actual layers is an array - } - } - - 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/LayerView/LayerView.py b/plugins/LayerView/LayerView.py deleted file mode 100755 index 04be97b747..0000000000 --- a/plugins/LayerView/LayerView.py +++ /dev/null @@ -1,495 +0,0 @@ -# Copyright (c) 2015 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import sys - -from UM.PluginRegistry import PluginRegistry -from UM.View.View import View -from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator -from UM.Resources import Resources -from UM.Event import Event, KeyEvent -from UM.Signal import Signal -from UM.Scene.Selection import Selection -from UM.Math.Color import Color -from UM.Mesh.MeshBuilder import MeshBuilder -from UM.Job import Job -from UM.Preferences import Preferences -from UM.Logger import Logger -from UM.View.GL.OpenGL import OpenGL -from UM.Message import Message -from UM.Application import Application -from UM.View.GL.OpenGLContext import OpenGLContext - -from cura.ConvexHullNode import ConvexHullNode -from cura.Settings.ExtruderManager import ExtruderManager - -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QApplication - -from . import LayerViewProxy - -from UM.i18n import i18nCatalog -catalog = i18nCatalog("cura") - -from . import LayerPass - -import numpy -import os.path - -## View used to display g-code paths. -class LayerView(View): - # Must match LayerView.qml - LAYER_VIEW_TYPE_MATERIAL_TYPE = 0 - LAYER_VIEW_TYPE_LINE_TYPE = 1 - - 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._busy = False - - self._ghost_shader = None - self._layer_pass = None - self._composite_pass = None - self._old_layer_bindings = None - self._layerview_composite_shader = None - self._old_composite_shader = None - - self._global_container_stack = None - self._proxy = LayerViewProxy.LayerViewProxy() - self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged) - - self._resetSettings() - self._legend_items = None - self._show_travel_moves = False - - 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", "Layer 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 - - def getActivity(self): - return self._activity - - def getLayerPass(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 = LayerPass.LayerPass(1, 1) - self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode")) - self._layer_pass.setLayerView(self) - return self._layer_pass - - def getCurrentLayer(self): - return self._current_layer_num - - def getMinimumLayer(self): - return self._minimum_layer_num - - def _onSceneChanged(self, node): - self.calculateMaxLayers() - - def getMaxLayers(self): - return self._max_layers - - busyChanged = Signal() - - def isBusy(self): - return self._busy - - def setBusy(self, busy): - if busy != self._busy: - self._busy = busy - self.busyChanged.emit() - - def resetLayerData(self): - self._current_layer_mesh = None - self._current_layer_jumps = None - - 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() - - ## Set the layer view type - # - # \param layer_view_type integer as in LayerView.qml and this class - def setLayerViewType(self, layer_view_type): - self._layer_view_type = layer_view_type - self.currentLayerNumChanged.emit() - - ## Return the layer view type, integer as in LayerView.qml and this class - def getLayerViewType(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 calculateMaxLayers(self): - scene = self.getController().getScene() - self._activity = True - - 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 - - min_layer_number = sys.maxsize - max_layer_number = -sys.maxsize - for layer_id in layer_data.getLayers(): - 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() - - maxLayersChanged = Signal() - currentLayerNumChanged = Signal() - globalStackChanged = Signal() - preferencesChanged = 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 LayerPass is created - layer_pass = self.getLayerPass() - self.getRenderer().addRenderPass(layer_pass) - - Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) - self._onGlobalStackChanged() - - if not self._layerview_composite_shader: - self._layerview_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("LayerView"), "layerview_composite.shader")) - theme = Application.getInstance().getTheme() - self._layerview_composite_shader.setUniformValue("u_background_color", Color(*theme.getColor("viewport_background").getRgb())) - self._layerview_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("layerview") - self._old_composite_shader = self._composite_pass.getCompositeShader() - self._composite_pass.setCompositeShader(self._layerview_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.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 _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.setLayerViewType(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/LayerView/LayerView.qml b/plugins/LayerView/LayerView.qml deleted file mode 100755 index 7261926bc5..0000000000 --- a/plugins/LayerView/LayerView.qml +++ /dev/null @@ -1,388 +0,0 @@ -// 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.LayerView.compatibilityMode) { - return UM.Theme.getSize("layerview_menu_size_compatibility").width; - } else { - return UM.Theme.getSize("layerview_menu_size").width; - } - } - height: { - if (UM.LayerView.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.LayerView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height) - } else { - return UM.Theme.getSize("layerview_menu_size").height + UM.LayerView.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: slider.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.LayerView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type") == 1 - 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.LayerView.compatibilityMode - Layout.fillWidth: true - color: UM.Theme.getColor("setting_control_text") - } - - ListModel // matches LayerView.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 // 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.LayerView.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.LayerView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type"); - updateLegends(currentIndex); - } - - function updateLegends(type_id) - { - // update visibility of legends - view_settings.show_legend = UM.LayerView.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.LayerView.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.LayerView.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.LayerView.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 - } - 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: typesLegenModel - Component.onCompleted: - { - typesLegenModel.append({ - label: catalog.i18nc("@label", "Show Travels"), - initialValue: view_settings.show_travel_moves, - preference: "layerview/show_travel_moves", - colorId: "layerview_move_combing" - }); - typesLegenModel.append({ - label: catalog.i18nc("@label", "Show Helpers"), - initialValue: view_settings.show_helpers, - preference: "layerview/show_helpers", - colorId: "layerview_support" - }); - typesLegenModel.append({ - label: catalog.i18nc("@label", "Show Shell"), - initialValue: view_settings.show_skin, - preference: "layerview/show_skin", - colorId: "layerview_inset_0" - }); - typesLegenModel.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.LayerView.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.LayerView.compatibilityMode - style: UM.Theme.styles.checkbox - } - - Repeater { - model: ListModel { - id: typesLegenModelNoCheck - Component.onCompleted: - { - typesLegenModelNoCheck.append({ - label: catalog.i18nc("@label", "Top / Bottom"), - colorId: "layerview_skin", - }); - typesLegenModelNoCheck.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") - } - } - } - - LayerSlider { - id: slider - - width: UM.Theme.getSize("slider_handle").width - height: UM.Theme.getSize("layerview_menu_size").height - - anchors { - top: parent.bottom - topMargin: UM.Theme.getSize("slider_layerview_margin").height - right: layerViewMenu.right - rightMargin: UM.Theme.getSize("slider_layerview_margin").width - } - - // custom properties - upperValue: UM.LayerView.currentLayer - lowerValue: UM.LayerView.minimumLayer - maximumValue: UM.LayerView.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") - handleLabelWidth: UM.Theme.getSize("slider_layerview_background").width - layersVisible: UM.LayerView.layerActivity && CuraApplication.platformActivity ? true : false - - // update values when layer data changes - Connections { - target: UM.LayerView - onMaxLayersChanged: slider.setUpperValue(UM.LayerView.currentLayer) - onMinimumLayerChanged: slider.setLowerValue(UM.LayerView.minimumLayer) - onCurrentLayerChanged: slider.setUpperValue(UM.LayerView.currentLayer) - } - - // make sure the slider handlers show the correct value after switching views - Component.onCompleted: { - slider.setLowerValue(UM.LayerView.minimumLayer) - slider.setUpperValue(UM.LayerView.currentLayer) - } - } - } - - FontMetrics { - id: fontMetrics - font: UM.Theme.getFont("default") - } -} diff --git a/plugins/LayerView/LayerViewProxy.py b/plugins/LayerView/LayerViewProxy.py deleted file mode 100644 index 4cf84117da..0000000000 --- a/plugins/LayerView/LayerViewProxy.py +++ /dev/null @@ -1,151 +0,0 @@ -from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty -from UM.FlameProfiler import pyqtSlot -from UM.Application import Application - -import LayerView - - -class LayerViewProxy(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() - - currentLayerChanged = pyqtSignal() - maxLayersChanged = pyqtSignal() - activityChanged = pyqtSignal() - globalStackChanged = pyqtSignal() - preferencesChanged = pyqtSignal() - busyChanged = pyqtSignal() - - @pyqtProperty(bool, notify=activityChanged) - def layerActivity(self): - active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: - return active_view.getActivity() - - @pyqtProperty(int, notify=maxLayersChanged) - def numLayers(self): - active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: - return active_view.getMaxLayers() - - @pyqtProperty(int, notify=currentLayerChanged) - def currentLayer(self): - active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: - return active_view.getCurrentLayer() - - @pyqtProperty(int, notify=currentLayerChanged) - def minimumLayer(self): - active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: - return active_view.getMinimumLayer() - - @pyqtProperty(bool, notify=busyChanged) - def busy(self): - active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: - return active_view.isBusy() - - return False - - @pyqtProperty(bool, notify=preferencesChanged) - def compatibilityMode(self): - active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: - return active_view.getCompatibilityMode() - return False - - @pyqtSlot(int) - def setCurrentLayer(self, layer_num): - active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: - active_view.setLayer(layer_num) - - @pyqtSlot(int) - def setMinimumLayer(self, layer_num): - active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: - active_view.setMinimumLayer(layer_num) - - @pyqtSlot(int) - def setLayerViewType(self, layer_view_type): - active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: - active_view.setLayerViewType(layer_view_type) - - @pyqtSlot(result=int) - def getLayerViewType(self): - active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: - return active_view.getLayerViewType() - return 0 - - # Opacity 0..1 - @pyqtSlot(int, float) - def setExtruderOpacity(self, extruder_nr, opacity): - active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: - active_view.setExtruderOpacity(extruder_nr, opacity) - - @pyqtSlot(int) - def setShowTravelMoves(self, show): - active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: - active_view.setShowTravelMoves(show) - - @pyqtSlot(int) - def setShowHelpers(self, show): - active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: - active_view.setShowHelpers(show) - - @pyqtSlot(int) - def setShowSkin(self, show): - active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: - active_view.setShowSkin(show) - - @pyqtSlot(int) - def setShowInfill(self, show): - active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: - active_view.setShowInfill(show) - - @pyqtProperty(int, notify=globalStackChanged) - def extruderCount(self): - active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: - return active_view.getExtruderCount() - return 0 - - def _layerActivityChanged(self): - self.activityChanged.emit() - - def _onLayerChanged(self): - self.currentLayerChanged.emit() - self._layerActivityChanged() - - def _onMaxLayersChanged(self): - self.maxLayersChanged.emit() - - def _onBusyChanged(self): - self.busyChanged.emit() - - def _onGlobalStackChanged(self): - self.globalStackChanged.emit() - - def _onPreferencesChanged(self): - self.preferencesChanged.emit() - - def _onActiveViewChanged(self): - active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: - active_view.currentLayerNumChanged.connect(self._onLayerChanged) - active_view.maxLayersChanged.connect(self._onMaxLayersChanged) - active_view.busyChanged.connect(self._onBusyChanged) - active_view.globalStackChanged.connect(self._onGlobalStackChanged) - active_view.preferencesChanged.connect(self._onPreferencesChanged) diff --git a/plugins/LayerView/__init__.py b/plugins/LayerView/__init__.py deleted file mode 100644 index da1a5aed19..0000000000 --- a/plugins/LayerView/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2015 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -from . import LayerView, LayerViewProxy -from PyQt5.QtQml import qmlRegisterType, qmlRegisterSingletonType - -from UM.i18n import i18nCatalog -catalog = i18nCatalog("cura") - -def getMetaData(): - return { - "view": { - "name": catalog.i18nc("@item:inlistbox", "Layer view"), - "view_panel": "LayerView.qml", - "weight": 2 - } - } - -def createLayerViewProxy(engine, script_engine): - return LayerViewProxy.LayerViewProxy() - -def register(app): - layer_view = LayerView.LayerView() - qmlRegisterSingletonType(LayerViewProxy.LayerViewProxy, "UM", 1, 0, "LayerView", layer_view.getProxy) - return { "view": LayerView.LayerView() } diff --git a/plugins/LayerView/layers.shader b/plugins/LayerView/layers.shader deleted file mode 100755 index d340773403..0000000000 --- a/plugins/LayerView/layers.shader +++ /dev/null @@ -1,156 +0,0 @@ -[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/LayerView/layers3d.shader b/plugins/LayerView/layers3d.shader deleted file mode 100755 index e8fe425c70..0000000000 --- a/plugins/LayerView/layers3d.shader +++ /dev/null @@ -1,264 +0,0 @@ -[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 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; - - 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 - - switch (u_layer_view_type) { - case 0: // "Material color" - v_color = a_material_color; - break; - case 1: // "Line type" - v_color = a_color; - 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 - -[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 diff --git a/plugins/LayerView/layerview_composite.shader b/plugins/LayerView/layerview_composite.shader deleted file mode 100644 index dcc02acc84..0000000000 --- a/plugins/LayerView/layerview_composite.shader +++ /dev/null @@ -1,148 +0,0 @@ -[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 diff --git a/plugins/LayerView/plugin.json b/plugins/LayerView/plugin.json deleted file mode 100644 index 232fe9c361..0000000000 --- a/plugins/LayerView/plugin.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "Layer View", - "author": "Ultimaker B.V.", - "version": "1.0.0", - "description": "Provides the Layer view.", - "api": 4, - "i18n-catalog": "cura" -} diff --git a/resources/themes/cura-dark/theme.json b/resources/themes/cura-dark/theme.json index e61c48bffd..5cfed426e5 100644 --- a/resources/themes/cura-dark/theme.json +++ b/resources/themes/cura-dark/theme.json @@ -133,6 +133,7 @@ "slider_groove_fill": [245, 245, 245, 255], "slider_handle": [255, 255, 255, 255], "slider_handle_hover": [77, 182, 226, 255], + "slider_handle_active": [68, 192, 255, 255], "slider_handle_border": [39, 44, 48, 255], "slider_text_background": [255, 255, 255, 255], @@ -194,6 +195,7 @@ "layerview_move_combing": [0, 0, 255, 255], "layerview_move_retraction": [128, 128, 255, 255], "layerview_support_interface": [64, 192, 255, 255], + "layerview_nozzle": [181, 166, 66, 120], "material_compatibility_warning": [255, 255, 255, 255], diff --git a/resources/themes/cura-light/styles.qml b/resources/themes/cura-light/styles.qml index 6d991c5541..ea9d184926 100755 --- a/resources/themes/cura-light/styles.qml +++ b/resources/themes/cura-light/styles.qml @@ -269,6 +269,7 @@ QtObject { arrowSize: Theme.getSize("button_tooltip_arrow").width color: Theme.getColor("button_tooltip") opacity: control.hovered ? 1.0 : 0.0; + visible: control.text != "" width: control.hovered ? button_tip.width + Theme.getSize("button_tooltip").width : 0 height: Theme.getSize("button_tooltip").height diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index f084e87da2..4197285dd8 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -183,6 +183,7 @@ "slider_groove_fill": [127, 127, 127, 255], "slider_handle": [0, 0, 0, 255], "slider_handle_hover": [77, 182, 226, 255], + "slider_handle_active": [68, 192, 255, 255], "slider_handle_border": [39, 44, 48, 255], "slider_text_background": [255, 255, 255, 255], @@ -271,7 +272,8 @@ "layerview_support_infill": [0, 255, 255, 255], "layerview_move_combing": [0, 0, 255, 255], "layerview_move_retraction": [128, 128, 255, 255], - "layerview_support_interface": [64, 192, 255, 255] + "layerview_support_interface": [64, 192, 255, 255], + "layerview_nozzle": [181, 166, 66, 50] }, "sizes": { From 2df06bbbb9d517d66b7fb5b722e4eddc3b0afc47 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 21 Nov 2017 10:51:57 +0100 Subject: [PATCH 2/3] 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 From abe9ba379575ba5dbe4556b4afa4d62ce6847309 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 21 Nov 2017 13:23:47 +0100 Subject: [PATCH 3/3] CURA-4526 Stop simulation when changing preferences. Hide nozzle node when in compatibility mode. --- plugins/SimulationView/SimulationPass.py | 2 +- plugins/SimulationView/SimulationView.qml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/SimulationView/SimulationPass.py b/plugins/SimulationView/SimulationPass.py index 24a13eaf7a..e3b840fc87 100644 --- a/plugins/SimulationView/SimulationPass.py +++ b/plugins/SimulationView/SimulationPass.py @@ -171,7 +171,7 @@ class SimulationPass(RenderPass): 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 not self._compatibility_mode and 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) diff --git a/plugins/SimulationView/SimulationView.qml b/plugins/SimulationView/SimulationView.qml index 4c7d99deec..67ca39d992 100644 --- a/plugins/SimulationView/SimulationView.qml +++ b/plugins/SimulationView/SimulationView.qml @@ -168,6 +168,7 @@ Item { layerTypeCombobox.currentIndex = UM.SimulationView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type"); layerTypeCombobox.updateLegends(layerTypeCombobox.currentIndex); + playButton.pauseSimulation(); 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");