From 97fd92665440297c9e24698a1a67e1be079f50a9 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Fri, 3 Nov 2017 17:46:12 +0100 Subject: [PATCH 01/10] CURA-4526 Add to SimulationView the same behavior than LayerView --- plugins/CuraEngineBackend/CuraEngineBackend.py | 2 +- plugins/CuraEngineBackend/ProcessSlicedLayersJob.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 914aa1dee0..cfbc1f429f 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -588,7 +588,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() in ("LayerView", "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..639ea27135 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() in ("LayerView", "SimulationView"): self._progress_message.show() Job.yieldThread() if self._abort_requested: @@ -219,7 +219,7 @@ class ProcessSlicedLayersJob(Job): self._progress_message.setProgress(100) view = Application.getInstance().getController().getActiveView() - if view.getPluginId() == "LayerView": + if view.getPluginId() in ("LayerView", "SimulationView"): view.resetLayerData() if self._progress_message: @@ -232,7 +232,7 @@ class ProcessSlicedLayersJob(Job): def _onActiveViewChanged(self): if self.isRunning(): - if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": + if Application.getInstance().getController().getActiveView().getPluginId() in ("LayerView", "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: From b7172ad9e36a9109592d8d8641f7c39de088c207 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 7 Nov 2017 17:43:27 +0100 Subject: [PATCH 02/10] Add return values to some functions that cause some crashes --- plugins/LayerView/LayerViewProxy.py | 39 ++++++++++++++++------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/plugins/LayerView/LayerViewProxy.py b/plugins/LayerView/LayerViewProxy.py index 4cf84117da..4cbff65040 100644 --- a/plugins/LayerView/LayerViewProxy.py +++ b/plugins/LayerView/LayerViewProxy.py @@ -23,64 +23,67 @@ class LayerViewProxy(QObject): @pyqtProperty(bool, notify=activityChanged) def layerActivity(self): active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: + if isinstance(active_view, LayerView.LayerView.LayerView): return active_view.getActivity() + return False @pyqtProperty(int, notify=maxLayersChanged) def numLayers(self): active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: + if isinstance(active_view, LayerView.LayerView.LayerView): return active_view.getMaxLayers() + return 0 @pyqtProperty(int, notify=currentLayerChanged) def currentLayer(self): active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: + if isinstance(active_view, LayerView.LayerView.LayerView): return active_view.getCurrentLayer() + return 0 @pyqtProperty(int, notify=currentLayerChanged) def minimumLayer(self): active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: + if isinstance(active_view, LayerView.LayerView.LayerView): return active_view.getMinimumLayer() + return 0 @pyqtProperty(bool, notify=busyChanged) def busy(self): active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: + if isinstance(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: + if isinstance(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: + if isinstance(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: + if isinstance(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: + if isinstance(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: + if isinstance(active_view, LayerView.LayerView.LayerView): return active_view.getLayerViewType() return 0 @@ -88,37 +91,37 @@ class LayerViewProxy(QObject): @pyqtSlot(int, float) def setExtruderOpacity(self, extruder_nr, opacity): active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: + if isinstance(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: + if isinstance(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: + if isinstance(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: + if isinstance(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: + if isinstance(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: + if isinstance(active_view, LayerView.LayerView.LayerView): return active_view.getExtruderCount() return 0 @@ -143,7 +146,7 @@ class LayerViewProxy(QObject): def _onActiveViewChanged(self): active_view = self._controller.getActiveView() - if type(active_view) == LayerView.LayerView.LayerView: + if isinstance(active_view, LayerView.LayerView.LayerView): active_view.currentLayerNumChanged.connect(self._onLayerChanged) active_view.maxLayersChanged.connect(self._onMaxLayersChanged) active_view.busyChanged.connect(self._onBusyChanged) From 1ff254194768bcb81888bcee17f8b9cc2bb0d25e Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 9 Nov 2017 10:34:35 +0100 Subject: [PATCH 03/10] Add nozzle color to the default themes --- plugins/CuraEngineBackend/ProcessSlicedLayersJob.py | 1 + resources/themes/cura-dark/theme.json | 1 + resources/themes/cura-light/theme.json | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index 639ea27135..ab93a01204 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -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) diff --git a/resources/themes/cura-dark/theme.json b/resources/themes/cura-dark/theme.json index e61c48bffd..8bf1455b60 100644 --- a/resources/themes/cura-dark/theme.json +++ b/resources/themes/cura-dark/theme.json @@ -194,6 +194,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/theme.json b/resources/themes/cura-light/theme.json index b41ea96846..166fd81dce 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -271,7 +271,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 aa9f9d5b88ef7e1da0885a82d23ec413bbb563b1 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Fri, 10 Nov 2017 16:59:06 +0100 Subject: [PATCH 04/10] Add line thickness and feedrate to the data sent from the backend --- cura/Layer.py | 4 ++-- cura/LayerDataBuilder.py | 12 ++++++++--- cura/LayerPolygon.py | 20 ++++++++++++++++--- plugins/CuraEngineBackend/Cura.proto | 4 ++++ .../ProcessSlicedLayersJob.py | 11 +++++----- plugins/GCodeReader/GCodeReader.py | 6 ++++-- 6 files changed, 42 insertions(+), 15 deletions(-) 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/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index ab93a01204..30fcf6cced 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -128,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 @@ -146,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) diff --git a/plugins/GCodeReader/GCodeReader.py b/plugins/GCodeReader/GCodeReader.py index 9107ec3258..c7aea0ddcc 100755 --- a/plugins/GCodeReader/GCodeReader.py +++ b/plugins/GCodeReader/GCodeReader.py @@ -114,9 +114,11 @@ 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) + line_feedrates = numpy.empty((count - 1, 1), numpy.float32) # TODO: need to calculate actual line width based on E values line_widths[:, 0] = 0.35 # Just a guess - line_thicknesses[:, 0] = layer_thickness + line_thicknesses[:, 0] = layer_thickness # TODO Same for all, but it should be calculated from the layer height differences + line_feedrates[:, 0] = 50 # TODO Now we use 50mm/s as a demo, it should be obtained from the GCode points = numpy.empty((count, 3), numpy.float32) i = 0 for point in path: @@ -127,7 +129,7 @@ class GCodeReader(MeshReader): line_widths[i - 1] = 0.1 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) From 99319b223cbc49f75b25c6e3e01ccddc3c6a9872 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 14 Nov 2017 15:28:31 +0100 Subject: [PATCH 05/10] CURA-4526 GCodeReader now reads the feedrate and layer thickness --- plugins/GCodeReader/GCodeReader.py | 54 +++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/plugins/GCodeReader/GCodeReader.py b/plugins/GCodeReader/GCodeReader.py index c7aea0ddcc..9ff4df4692 100755 --- a/plugins/GCodeReader/GCodeReader.py +++ b/plugins/GCodeReader/GCodeReader.py @@ -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 @@ -96,7 +97,7 @@ class GCodeReader(MeshReader): def _createPolygon(self, layer_thickness, path, extruder_offsets): countvalid = 0 for point in path: - if point[3] > 0: + if point[4] > 0: countvalid += 1 if countvalid >= 2: # we know what to do now, no need to count further @@ -117,16 +118,19 @@ class GCodeReader(MeshReader): line_feedrates = numpy.empty((count - 1, 1), numpy.float32) # TODO: need to calculate actual line width based on E values line_widths[:, 0] = 0.35 # Just a guess - line_thicknesses[:, 0] = layer_thickness # TODO Same for all, but it should be calculated from the layer height differences - line_feedrates[:, 0] = 50 # TODO Now we use 50mm/s as a demo, it should be obtained from the GCode + line_thicknesses[:, 0] = layer_thickness points = numpy.empty((count, 3), numpy.float32) i = 0 for point in path: points[i, :] = [point[0] + extruder_offsets[0], point[2], -point[1] - extruder_offsets[1]] 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[4] + if point[4] 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] = _calculateLineWidth(layer_thickness) i += 1 this_poly = LayerPolygon(self._extruder_number, line_types, points, line_widths, line_thicknesses, line_feedrates) @@ -135,27 +139,43 @@ class GCodeReader(MeshReader): this_layer.polygons.append(this_poly) return True + def _calculateLineWidth(self, current_position, previous_position, layer_thickness): + # Area of the filament + filament_diameter = 2.85 + Af = (filament_diameter / 2) ** 2 * numpy.pi + # Length of the extruded filament + de = current_position.e - previous_position.e + # Volumne of the extruded filament + dVe = de * Af + # Length of the printed line + dX = numpy.sqrt((current_position.x - previous_position.x)**2 + (current_position.y - previous_position.y)**2) + # Area of the printed line. This area is a ellipse + Ae = dVe / dX + # This area is a ellipse with: r1 = layer_thickness, r2 = layer_width + return Ae / (layer_thickness * numpy.pi) + def _gCode0(self, position, params, path): - x, y, z, e = position + x, y, z, f, e = position x = params.x if params.x is not None else x y = params.y if params.y is not None else y z = params.z if params.z is not None else position.z + f = params.f if params.f is not None else position.f if params.e is not None: if params.e > e[self._extruder_number]: - path.append([x, y, z, self._layer_type]) # extrusion + path.append([x, y, z, f, self._layer_type]) # extrusion else: - path.append([x, y, z, LayerPolygon.MoveRetractionType]) # retraction + path.append([x, y, z, f, LayerPolygon.MoveRetractionType]) # retraction e[self._extruder_number] = params.e # 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, LayerPolygon.MoveCombingType]) + return self._position(x, y, z, f, e) # G0 and G1 should be handled exactly the same. _gCode1 = _gCode0 @@ -166,6 +186,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) ## Reset the current position to the values specified. @@ -177,6 +198,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, 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): @@ -184,7 +206,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 @@ -196,11 +218,13 @@ 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 (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 @@ -262,7 +286,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: From 50308c6e39732fab609d1c5102bebb1e3e7a2227 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Wed, 15 Nov 2017 13:03:36 +0100 Subject: [PATCH 06/10] CURA-4526 Calculate the line width from the GCode --- plugins/GCodeReader/GCodeReader.py | 65 +++++++++++++++++------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/plugins/GCodeReader/GCodeReader.py b/plugins/GCodeReader/GCodeReader.py index 9ff4df4692..ca4042351d 100755 --- a/plugins/GCodeReader/GCodeReader.py +++ b/plugins/GCodeReader/GCodeReader.py @@ -49,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 +99,7 @@ class GCodeReader(MeshReader): def _createPolygon(self, layer_thickness, path, extruder_offsets): countvalid = 0 for point in path: - if point[4] > 0: + if point[5] > 0: countvalid += 1 if countvalid >= 2: # we know what to do now, no need to count further @@ -116,21 +118,22 @@ class GCodeReader(MeshReader): line_widths = numpy.empty((count - 1, 1), numpy.float32) line_thicknesses = numpy.empty((count - 1, 1), numpy.float32) line_feedrates = numpy.empty((count - 1, 1), numpy.float32) - # TODO: need to calculate actual line width based on E values 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_feedrates[i - 1] = point[3] - line_types[i - 1] = point[4] - if point[4] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]: + 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] = _calculateLineWidth(layer_thickness) + 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, line_feedrates) @@ -139,20 +142,27 @@ class GCodeReader(MeshReader): this_layer.polygons.append(this_poly) return True - def _calculateLineWidth(self, current_position, previous_position, layer_thickness): + def _calculateLineWidth(self, current_point, previous_point, current_extrusion, previous_extrusion, layer_thickness): # Area of the filament - filament_diameter = 2.85 - Af = (filament_diameter / 2) ** 2 * numpy.pi + Af = (self._filament_diameter / 2) ** 2 * numpy.pi # Length of the extruded filament - de = current_position.e - previous_position.e + de = current_extrusion - previous_extrusion # Volumne of the extruded filament dVe = de * Af # Length of the printed line - dX = numpy.sqrt((current_position.x - previous_position.x)**2 + (current_position.y - previous_position.y)**2) - # Area of the printed line. This area is a ellipse + 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 ellipse with: r1 = layer_thickness, r2 = layer_width - return Ae / (layer_thickness * numpy.pi) + # 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, f, e = position @@ -163,9 +173,9 @@ class GCodeReader(MeshReader): if params.e is not None: if params.e > e[self._extruder_number]: - path.append([x, y, z, f, 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, f, 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] = params.e # Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions @@ -174,7 +184,7 @@ class GCodeReader(MeshReader): self._current_layer_thickness = z - self._previous_z # allow a tiny overlap self._previous_z = z else: - path.append([x, y, z, f, LayerPolygon.MoveCombingType]) + 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. @@ -193,6 +203,8 @@ 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, @@ -231,6 +243,7 @@ class GCodeReader(MeshReader): 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 @@ -249,6 +262,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 @@ -319,6 +334,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):]) @@ -334,17 +350,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: @@ -353,8 +364,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() From 0f6d65950a8ea017f17d44a019528c55752410ec Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Fri, 17 Nov 2017 13:18:44 +0100 Subject: [PATCH 07/10] CURA-4526 Add colors to themes for selected handles --- resources/themes/cura-dark/theme.json | 1 + resources/themes/cura-light/styles.qml | 1 + resources/themes/cura-light/theme.json | 1 + 3 files changed, 3 insertions(+) diff --git a/resources/themes/cura-dark/theme.json b/resources/themes/cura-dark/theme.json index 8bf1455b60..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], 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 21fd23316d..12f7df906b 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], From 98d48978a860d91457af87430941f57df4292066 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Fri, 17 Nov 2017 14:27:46 +0100 Subject: [PATCH 08/10] CURA-4526 Add the SimulationView plugin to Cura and deleting LayerView plugin --- cura/CuraApplication.py | 4 +- .../CuraEngineBackend/CuraEngineBackend.py | 2 +- .../ProcessSlicedLayersJob.py | 6 +- plugins/LayerView/LayerPass.py | 113 ---- plugins/LayerView/LayerViewProxy.py | 154 ----- plugins/LayerView/__init__.py | 25 - .../LayerSlider.qml | 637 +++++++++--------- plugins/SimulationView/NozzleNode.py | 49 ++ plugins/SimulationView/PathSlider.qml | 161 +++++ plugins/SimulationView/SimulationPass.py | 187 +++++ .../SimulationSliderLabel.qml} | 207 +++--- .../SimulationView.py} | 216 ++++-- .../SimulationView.qml} | 344 ++++++++-- plugins/SimulationView/SimulationViewProxy.py | 246 +++++++ plugins/SimulationView/__init__.py | 26 + .../layers.shader | 0 .../layers3d.shader | 29 + plugins/SimulationView/layers3d_shadow.shader | 256 +++++++ plugins/SimulationView/layers_shadow.shader | 156 +++++ .../{LayerView => SimulationView}/plugin.json | 4 +- plugins/SimulationView/resources/nozzle.stl | Bin 0 -> 210284 bytes .../resources/simulation_pause.svg | 79 +++ .../resources/simulation_resume.svg | 82 +++ .../simulationview_composite.shader} | 0 24 files changed, 2162 insertions(+), 821 deletions(-) delete mode 100755 plugins/LayerView/LayerPass.py delete mode 100644 plugins/LayerView/LayerViewProxy.py delete mode 100644 plugins/LayerView/__init__.py rename plugins/{LayerView => SimulationView}/LayerSlider.qml (85%) create mode 100644 plugins/SimulationView/NozzleNode.py create mode 100644 plugins/SimulationView/PathSlider.qml create mode 100644 plugins/SimulationView/SimulationPass.py rename plugins/{LayerView/LayerSliderLabel.qml => SimulationView/SimulationSliderLabel.qml} (88%) rename plugins/{LayerView/LayerView.py => SimulationView/SimulationView.py} (74%) mode change 100755 => 100644 rename plugins/{LayerView/LayerView.qml => SimulationView/SimulationView.qml} (51%) mode change 100755 => 100644 create mode 100644 plugins/SimulationView/SimulationViewProxy.py create mode 100644 plugins/SimulationView/__init__.py rename plugins/{LayerView => SimulationView}/layers.shader (100%) mode change 100755 => 100644 rename plugins/{LayerView => SimulationView}/layers3d.shader (92%) mode change 100755 => 100644 create mode 100644 plugins/SimulationView/layers3d_shadow.shader create mode 100644 plugins/SimulationView/layers_shadow.shader rename plugins/{LayerView => SimulationView}/plugin.json (54%) 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 rename plugins/{LayerView/layerview_composite.shader => SimulationView/simulationview_composite.shader} (100%) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 41358b83f5..bfd2b0c50a 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -217,7 +217,7 @@ class CuraApplication(QtApplication): "CuraEngineBackend", "UserAgreement", "SolidView", - "LayerView", + "SimulationView", "STLReader", "SelectionTool", "CameraTool", @@ -1383,7 +1383,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/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 44e028093b..a5a1a5b584 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -588,7 +588,7 @@ class CuraEngineBackend(QObject, Backend): def _onActiveViewChanged(self): if Application.getInstance().getController().getActiveView(): view = Application.getInstance().getController().getActiveView() - if view.getPluginId() in ("LayerView", "SimulationView"): # 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 30fcf6cced..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() in ("LayerView", "SimulationView"): + if Application.getInstance().getController().getActiveView().getPluginId() == "SimulationView": self._progress_message.show() Job.yieldThread() if self._abort_requested: @@ -221,7 +221,7 @@ class ProcessSlicedLayersJob(Job): self._progress_message.setProgress(100) view = Application.getInstance().getController().getActiveView() - if view.getPluginId() in ("LayerView", "SimulationView"): + if view.getPluginId() == "SimulationView": view.resetLayerData() if self._progress_message: @@ -234,7 +234,7 @@ class ProcessSlicedLayersJob(Job): def _onActiveViewChanged(self): if self.isRunning(): - if Application.getInstance().getController().getActiveView().getPluginId() in ("LayerView", "SimulationView"): + 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/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/LayerViewProxy.py b/plugins/LayerView/LayerViewProxy.py deleted file mode 100644 index 4cbff65040..0000000000 --- a/plugins/LayerView/LayerViewProxy.py +++ /dev/null @@ -1,154 +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 isinstance(active_view, LayerView.LayerView.LayerView): - return active_view.getActivity() - return False - - @pyqtProperty(int, notify=maxLayersChanged) - def numLayers(self): - active_view = self._controller.getActiveView() - if isinstance(active_view, LayerView.LayerView.LayerView): - return active_view.getMaxLayers() - return 0 - - @pyqtProperty(int, notify=currentLayerChanged) - def currentLayer(self): - active_view = self._controller.getActiveView() - if isinstance(active_view, LayerView.LayerView.LayerView): - return active_view.getCurrentLayer() - return 0 - - @pyqtProperty(int, notify=currentLayerChanged) - def minimumLayer(self): - active_view = self._controller.getActiveView() - if isinstance(active_view, LayerView.LayerView.LayerView): - return active_view.getMinimumLayer() - return 0 - - @pyqtProperty(bool, notify=busyChanged) - def busy(self): - active_view = self._controller.getActiveView() - if isinstance(active_view, LayerView.LayerView.LayerView): - return active_view.isBusy() - return False - - @pyqtProperty(bool, notify=preferencesChanged) - def compatibilityMode(self): - active_view = self._controller.getActiveView() - if isinstance(active_view, LayerView.LayerView.LayerView): - return active_view.getCompatibilityMode() - return False - - @pyqtSlot(int) - def setCurrentLayer(self, layer_num): - active_view = self._controller.getActiveView() - if isinstance(active_view, LayerView.LayerView.LayerView): - active_view.setLayer(layer_num) - - @pyqtSlot(int) - def setMinimumLayer(self, layer_num): - active_view = self._controller.getActiveView() - if isinstance(active_view, LayerView.LayerView.LayerView): - active_view.setMinimumLayer(layer_num) - - @pyqtSlot(int) - def setLayerViewType(self, layer_view_type): - active_view = self._controller.getActiveView() - if isinstance(active_view, LayerView.LayerView.LayerView): - active_view.setLayerViewType(layer_view_type) - - @pyqtSlot(result=int) - def getLayerViewType(self): - active_view = self._controller.getActiveView() - if isinstance(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 isinstance(active_view, LayerView.LayerView.LayerView): - active_view.setExtruderOpacity(extruder_nr, opacity) - - @pyqtSlot(int) - def setShowTravelMoves(self, show): - active_view = self._controller.getActiveView() - if isinstance(active_view, LayerView.LayerView.LayerView): - active_view.setShowTravelMoves(show) - - @pyqtSlot(int) - def setShowHelpers(self, show): - active_view = self._controller.getActiveView() - if isinstance(active_view, LayerView.LayerView.LayerView): - active_view.setShowHelpers(show) - - @pyqtSlot(int) - def setShowSkin(self, show): - active_view = self._controller.getActiveView() - if isinstance(active_view, LayerView.LayerView.LayerView): - active_view.setShowSkin(show) - - @pyqtSlot(int) - def setShowInfill(self, show): - active_view = self._controller.getActiveView() - if isinstance(active_view, LayerView.LayerView.LayerView): - active_view.setShowInfill(show) - - @pyqtProperty(int, notify=globalStackChanged) - def extruderCount(self): - active_view = self._controller.getActiveView() - if isinstance(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 isinstance(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/LayerSlider.qml b/plugins/SimulationView/LayerSlider.qml similarity index 85% rename from plugins/LayerView/LayerSlider.qml rename to plugins/SimulationView/LayerSlider.qml index 9abeb01148..22f9d91340 100644 --- a/plugins/LayerView/LayerSlider.qml +++ b/plugins/SimulationView/LayerSlider.qml @@ -1,312 +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 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 - } - } -} +// 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..4963568935 --- /dev/null +++ b/plugins/SimulationView/SimulationPass.py @@ -0,0 +1,187 @@ +# 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 plugins.SimulationView.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/LayerView/LayerSliderLabel.qml b/plugins/SimulationView/SimulationSliderLabel.qml similarity index 88% rename from plugins/LayerView/LayerSliderLabel.qml rename to plugins/SimulationView/SimulationSliderLabel.qml index c989679285..1c8daf867f 100644 --- a/plugins/LayerView/LayerSliderLabel.qml +++ b/plugins/SimulationView/SimulationSliderLabel.qml @@ -1,103 +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 - - 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 - } -} +// 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/LayerView/LayerView.py b/plugins/SimulationView/SimulationView.py old mode 100755 new mode 100644 similarity index 74% rename from plugins/LayerView/LayerView.py rename to plugins/SimulationView/SimulationView.py index 04be97b747..90f64a8224 --- a/plugins/LayerView/LayerView.py +++ b/plugins/SimulationView/SimulationView.py @@ -1,46 +1,46 @@ -# Copyright (c) 2015 Ultimaker B.V. +# Copyright (c) 2017 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.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.Math.Vector import Vector +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.SceneNode import SceneNode +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 -catalog = i18nCatalog("cura") +from cura.ConvexHullNode import ConvexHullNode +from plugins.SimulationView.NozzleNode import NozzleNode +from . import SimulationPass, SimulationViewProxy -from . import LayerPass +catalog = i18nCatalog("cura") import numpy import os.path ## View used to display g-code paths. -class LayerView(View): - # Must match LayerView.qml +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__() @@ -54,22 +54,29 @@ class LayerView(View): 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._layerview_composite_shader = None + self._simulationview_composite_shader = None self._old_composite_shader = None self._global_container_stack = None - self._proxy = LayerViewProxy.LayerViewProxy() + self._proxy = SimulationViewProxy.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) @@ -91,7 +98,7 @@ class LayerView(View): 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")) + 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 @@ -101,17 +108,24 @@ class LayerView(View): self._show_helpers = 1 self._show_skin = 1 self._show_infill = 1 + self.resetLayerData() def getActivity(self): return self._activity - def getLayerPass(self): + 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 = LayerPass.LayerPass(1, 1) + self._layer_pass = SimulationPass.SimulationPass(1, 1) self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode")) - self._layer_pass.setLayerView(self) + self._layer_pass.setSimulationView(self) return self._layer_pass def getCurrentLayer(self): @@ -120,13 +134,26 @@ class LayerView(View): def getMinimumLayer(self): return self._minimum_layer_num - def _onSceneChanged(self, node): - self.calculateMaxLayers() - def getMaxLayers(self): return self._max_layers - busyChanged = Signal() + 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 @@ -136,9 +163,19 @@ class LayerView(View): 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() @@ -186,15 +223,43 @@ class LayerView(View): 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 LayerView.qml and this class - def setLayerViewType(self, 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 LayerView.qml and this class - def getLayerViewType(self): + ## 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 @@ -243,9 +308,20 @@ class LayerView(View): 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._activity = True self._old_max_layers = self._max_layers ## Recalculate num max layers @@ -255,9 +331,16 @@ class LayerView(View): 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: @@ -281,10 +364,32 @@ class LayerView(View): 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. @@ -308,26 +413,31 @@ class LayerView(View): return True if event.type == Event.ViewActivateEvent: - # Make sure the LayerPass is created - layer_pass = self.getLayerPass() + # 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._layerview_composite_shader: - self._layerview_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("LayerView"), "layerview_composite.shader")) + 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._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())) + 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("layerview") + self._composite_pass.getLayerBindings().append("simulationview") self._old_composite_shader = self._composite_pass.getCompositeShader() - self._composite_pass.setCompositeShader(self._layerview_composite_shader) + self._composite_pass.setCompositeShader(self._simulationview_composite_shader) elif event.type == Event.ViewDeactivateEvent: self._wireprint_warning_message.hide() @@ -335,6 +445,7 @@ class LayerView(View): 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) @@ -364,6 +475,9 @@ class LayerView(View): else: self._wireprint_warning_message.hide() + def _onCurrentLayerNumChanged(self): + self.calculateMaxPathsOnLayer(self._current_layer_num) + def _startUpdateTopLayers(self): if not self._compatibility_mode: return @@ -397,7 +511,7 @@ class LayerView(View): 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")))); + 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: diff --git a/plugins/LayerView/LayerView.qml b/plugins/SimulationView/SimulationView.qml old mode 100755 new mode 100644 similarity index 51% rename from plugins/LayerView/LayerView.qml rename to plugins/SimulationView/SimulationView.qml index 7261926bc5..e2e0dc3aed --- a/plugins/LayerView/LayerView.qml +++ b/plugins/SimulationView/SimulationView.qml @@ -13,19 +13,19 @@ Item { id: base width: { - if (UM.LayerView.compatibilityMode) { + 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.LayerView.compatibilityMode) { + 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.LayerView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height) + 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.LayerView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height) + return UM.Theme.getSize("layerview_menu_size").height + UM.SimulationView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height) } } @@ -46,7 +46,7 @@ Item anchors.top: parent.top width: parent.width height: parent.height - z: slider.z - 1 + z: layerSlider.z - 1 color: UM.Theme.getColor("tool_panel_background") borderWidth: UM.Theme.getSize("default_lining").width borderColor: UM.Theme.getColor("lining") @@ -61,7 +61,8 @@ Item 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 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") @@ -79,12 +80,12 @@ Item anchors.left: parent.left text: catalog.i18nc("@label","Color scheme") font: UM.Theme.getFont("default"); - visible: !UM.LayerView.compatibilityMode + visible: !UM.SimulationView.compatibilityMode Layout.fillWidth: true color: UM.Theme.getColor("setting_control_text") } - ListModel // matches LayerView.py + ListModel // matches SimulationView.py { id: layerViewTypes } @@ -97,7 +98,15 @@ Item }) layerViewTypes.append({ text: catalog.i18nc("@label:listbox", "Line Type"), - type_id: 1 // these ids match the switching in the shader + 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 }) } @@ -108,7 +117,7 @@ Item Layout.fillWidth: true Layout.preferredWidth: UM.Theme.getSize("layerview_row").width model: layerViewTypes - visible: !UM.LayerView.compatibilityMode + visible: !UM.SimulationView.compatibilityMode style: UM.Theme.styles.combobox anchors.right: parent.right anchors.rightMargin: 10 * screenScaleFactor @@ -120,14 +129,14 @@ Item Component.onCompleted: { - currentIndex = UM.LayerView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type"); + 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.LayerView.compatibilityMode || (type_id == 1); + view_settings.show_legend = UM.SimulationView.compatibilityMode || (type_id == 1); } } @@ -139,7 +148,7 @@ Item text: catalog.i18nc("@label","Compatibility Mode") font: UM.Theme.getFont("default") color: UM.Theme.getColor("text") - visible: UM.LayerView.compatibilityMode + visible: UM.SimulationView.compatibilityMode Layout.fillWidth: true Layout.preferredHeight: UM.Theme.getSize("layerview_row").height Layout.preferredWidth: UM.Theme.getSize("layerview_row").width @@ -157,7 +166,7 @@ Item target: UM.Preferences onPreferenceChanged: { - layerTypeCombobox.currentIndex = UM.LayerView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type"); + 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"); @@ -178,7 +187,7 @@ Item 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 + visible: !UM.SimulationView.compatibilityMode enabled: index + 1 <= 4 Rectangle { anchors.verticalCenter: parent.verticalCenter @@ -190,7 +199,7 @@ Item radius: width / 2 border.width: UM.Theme.getSize("default_lining").width border.color: UM.Theme.getColor("lining") - visible: !view_settings.show_legend + 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 @@ -213,28 +222,28 @@ Item Repeater { model: ListModel { - id: typesLegenModel + id: typesLegendModel Component.onCompleted: { - typesLegenModel.append({ + typesLegendModel.append({ label: catalog.i18nc("@label", "Show Travels"), initialValue: view_settings.show_travel_moves, preference: "layerview/show_travel_moves", colorId: "layerview_move_combing" }); - typesLegenModel.append({ + typesLegendModel.append({ label: catalog.i18nc("@label", "Show Helpers"), initialValue: view_settings.show_helpers, preference: "layerview/show_helpers", colorId: "layerview_support" }); - typesLegenModel.append({ + typesLegendModel.append({ label: catalog.i18nc("@label", "Show Shell"), initialValue: view_settings.show_skin, preference: "layerview/show_skin", colorId: "layerview_inset_0" }); - typesLegenModel.append({ + typesLegendModel.append({ label: catalog.i18nc("@label", "Show Infill"), initialValue: view_settings.show_infill, preference: "layerview/show_infill", @@ -285,7 +294,7 @@ Item 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 + visible: UM.SimulationView.compatibilityMode style: UM.Theme.styles.checkbox } CheckBox { @@ -294,20 +303,20 @@ Item UM.Preferences.setValue("view/top_layer_count", checked ? 5 : 1); } text: catalog.i18nc("@label", "Show 5 Detailed Layers On Top") - visible: UM.LayerView.compatibilityMode + visible: UM.SimulationView.compatibilityMode style: UM.Theme.styles.checkbox } Repeater { model: ListModel { - id: typesLegenModelNoCheck + id: typesLegendModelNoCheck Component.onCompleted: { - typesLegenModelNoCheck.append({ + typesLegendModelNoCheck.append({ label: catalog.i18nc("@label", "Top / Bottom"), colorId: "layerview_skin", }); - typesLegenModelNoCheck.append({ + typesLegendModelNoCheck.append({ label: catalog.i18nc("@label", "Inner Wall"), colorId: "layerview_inset_x", }); @@ -336,47 +345,272 @@ Item 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) + } + } + } } - LayerSlider { - id: slider + Item { + id: slidersBox - width: UM.Theme.getSize("slider_handle").width - height: UM.Theme.getSize("layerview_menu_size").height + width: parent.width + visible: UM.SimulationView.layerActivity && CuraApplication.platformActivity anchors { top: parent.bottom topMargin: UM.Theme.getSize("slider_layerview_margin").height - right: layerViewMenu.right - rightMargin: UM.Theme.getSize("slider_layerview_margin").width + left: parent.left } - // 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 + PathSlider { + id: pathSlider - // 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) + 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) + } } - // make sure the slider handlers show the correct value after switching views - Component.onCompleted: { - slider.setLowerValue(UM.LayerView.minimumLayer) - slider.setUpperValue(UM.LayerView.currentLayer) + 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() + status = 1 + } + } + } + + 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 + if (currentPath >= numPaths) { + if (currentLayer >= numLayers) { + playButton.pauseSimulation() + } + else { + UM.SimulationView.setCurrentLayer(currentLayer+1) + UM.SimulationView.setCurrentPath(0) + } + } + else { + UM.SimulationView.setCurrentPath(currentPath+1) + } } } } diff --git a/plugins/SimulationView/SimulationViewProxy.py b/plugins/SimulationView/SimulationViewProxy.py new file mode 100644 index 0000000000..21a962104d --- /dev/null +++ b/plugins/SimulationView/SimulationViewProxy.py @@ -0,0 +1,246 @@ +# 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() + + 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): + 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/LayerView/layers.shader b/plugins/SimulationView/layers.shader old mode 100755 new mode 100644 similarity index 100% rename from plugins/LayerView/layers.shader rename to plugins/SimulationView/layers.shader diff --git a/plugins/LayerView/layers3d.shader b/plugins/SimulationView/layers3d.shader old mode 100755 new mode 100644 similarity index 92% rename from plugins/LayerView/layers3d.shader rename to plugins/SimulationView/layers3d.shader index e8fe425c70..f377fca055 --- a/plugins/LayerView/layers3d.shader +++ b/plugins/SimulationView/layers3d.shader @@ -6,6 +6,10 @@ vertex41core = 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 @@ -18,6 +22,8 @@ vertex41core = 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; @@ -32,6 +38,15 @@ vertex41core = 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; @@ -48,6 +63,12 @@ vertex41core = 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; @@ -247,6 +268,12 @@ 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 @@ -262,3 +289,5 @@ 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/LayerView/plugin.json b/plugins/SimulationView/plugin.json similarity index 54% rename from plugins/LayerView/plugin.json rename to plugins/SimulationView/plugin.json index 232fe9c361..0e7bec0626 100644 --- a/plugins/LayerView/plugin.json +++ b/plugins/SimulationView/plugin.json @@ -1,8 +1,8 @@ { - "name": "Layer View", + "name": "Simulation View", "author": "Ultimaker B.V.", "version": "1.0.0", - "description": "Provides the Layer view.", + "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/LayerView/layerview_composite.shader b/plugins/SimulationView/simulationview_composite.shader similarity index 100% rename from plugins/LayerView/layerview_composite.shader rename to plugins/SimulationView/simulationview_composite.shader From 56db10d9cb9315f20dc1ba92223dd76725db0df1 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Mon, 20 Nov 2017 11:24:25 +0100 Subject: [PATCH 09/10] CURA-4526 Change simulation behavior when clicking on Play button --- plugins/SimulationView/SimulationView.qml | 41 ++++++++++++++++++----- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/plugins/SimulationView/SimulationView.qml b/plugins/SimulationView/SimulationView.qml index e2e0dc3aed..4c7d99deec 100644 --- a/plugins/SimulationView/SimulationView.qml +++ b/plugins/SimulationView/SimulationView.qml @@ -583,7 +583,6 @@ Item UM.SimulationView.setSimulationRunning(true) iconSource = "./resources/simulation_pause.svg" simulationTimer.start() - status = 1 } } } @@ -599,18 +598,42 @@ Item var numPaths = UM.SimulationView.numPaths var currentLayer = UM.SimulationView.currentLayer var numLayers = UM.SimulationView.numLayers - if (currentPath >= numPaths) { - if (currentLayer >= numLayers) { - playButton.pauseSimulation() - } - else { - UM.SimulationView.setCurrentLayer(currentLayer+1) + // 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) + } } - 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 } } } From 20c21d6e79259804ba3684f193514cec8d5b5587 Mon Sep 17 00:00:00 2001 From: Aleksei S Date: Mon, 20 Nov 2017 13:52:22 +0100 Subject: [PATCH 10/10] Added "Signal.disconect" after changing views CURA-4526 --- plugins/SimulationView/SimulationViewProxy.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plugins/SimulationView/SimulationViewProxy.py b/plugins/SimulationView/SimulationViewProxy.py index 21a962104d..2bd707293f 100644 --- a/plugins/SimulationView/SimulationViewProxy.py +++ b/plugins/SimulationView/SimulationViewProxy.py @@ -16,6 +16,8 @@ class SimulationViewProxy(QObject): self._controller.activeViewChanged.connect(self._onActiveViewChanged) self._onActiveViewChanged() + self.is_simulationView_selected = False + currentLayerChanged = pyqtSignal() currentPathChanged = pyqtSignal() maxLayersChanged = pyqtSignal() @@ -236,6 +238,19 @@ class SimulationViewProxy(QObject): 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)