diff --git a/cura/Layer.py b/cura/Layer.py index 904e5528a3..4e38a6eba9 100644 --- a/cura/Layer.py +++ b/cura/Layer.py @@ -35,24 +35,31 @@ class Layer: def setThickness(self, thickness): self._thickness = thickness - def vertexCount(self): + def lineMeshVertexCount(self): result = 0 for polygon in self._polygons: - result += polygon.vertexCount() + result += polygon.lineMeshVertexCount() return result - def build(self, offset, vertices, colors, indices): - result = offset + def lineMeshElementCount(self): + result = 0 for polygon in self._polygons: - if polygon.type == LayerPolygon.InfillType or polygon.type == LayerPolygon.MoveCombingType or polygon.type == LayerPolygon.MoveRetractionType: - continue + result += polygon.lineMeshElementCount() - polygon.build(result, vertices, colors, indices) - result += polygon.vertexCount() + return result + + def build(self, vertex_offset, index_offset, vertices, colors, 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, indices) + result_vertex_offset += polygon.lineMeshVertexCount() + result_index_offset += polygon.lineMeshElementCount() self._element_count += polygon.elementCount - return result + return (result_vertex_offset, result_index_offset) def createMesh(self): return self.createMeshOrJumps(True) @@ -60,40 +67,52 @@ class Layer: def createJumps(self): return self.createMeshOrJumps(False) + # Defines the two triplets of local point indices to use to draw the two faces for each line segment in createMeshOrJump + __index_pattern = numpy.array([[0, 3, 2, 0, 1, 3]], dtype = numpy.int32 ) + def createMeshOrJumps(self, make_mesh): builder = MeshBuilder() - + + line_count = 0 + if make_mesh: + for polygon in self._polygons: + line_count += polygon.meshLineCount + else: + for polygon in self._polygons: + line_count += polygon.jumpCount + + + # Reserve the neccesary space for the data upfront + builder.reserveFaceAndVertexCount(2 * line_count, 4 * line_count) + for polygon in self._polygons: - if make_mesh and (polygon.type == LayerPolygon.MoveCombingType or polygon.type == LayerPolygon.MoveRetractionType): - continue - if not make_mesh and not (polygon.type == LayerPolygon.MoveCombingType or polygon.type == LayerPolygon.MoveRetractionType): - continue + # Filter out the types of lines we are not interesed in depending on whether we are drawing the mesh or the jumps. + index_mask = numpy.logical_not(polygon.jumpMask) if make_mesh else polygon.jumpMask - poly_color = polygon.getColor() + # Create an array with rows [p p+1] and only keep those we whant to draw based on make_mesh + points = numpy.concatenate((polygon.data[:-1], polygon.data[1:]), 1)[index_mask.ravel()] + # Line types of the points we want to draw + line_types = polygon.types[index_mask] + + # Shift the z-axis according to previous implementation. + if make_mesh: + points[polygon.isInfillOrSkinType(line_types), 1::3] -= 0.01 + else: + points[:, 1::3] += 0.01 - points = numpy.copy(polygon.data) - if polygon.type == LayerPolygon.InfillType or polygon.type == LayerPolygon.SkinType or polygon.type == LayerPolygon.SupportInfillType: - points[:,1] -= 0.01 - if polygon.type == LayerPolygon.MoveCombingType or polygon.type == LayerPolygon.MoveRetractionType: - points[:,1] += 0.01 + # Create an array with normals and tile 2 copies to match size of points variable + normals = numpy.tile( polygon.getNormals()[index_mask.ravel()], (1, 2)) - normals = polygon.getNormals() + # Scale all normals by the line width of the current line so we can easily offset. + normals *= (polygon.lineWidths[index_mask.ravel()] / 2) - # Scale all by the line width of the polygon so we can easily offset. - normals *= (polygon.lineWidth / 2) + # Create 4 points to draw each line segment, points +- normals results in 2 points each. Reshape to one point per line + f_points = numpy.concatenate((points-normals, points+normals), 1).reshape((-1, 3)) + # __index_pattern defines which points to use to draw the two faces for each lines egment, the following linesegment is offset by 4 + f_indices = ( self.__index_pattern + numpy.arange(0, 4 * len(normals), 4, dtype=numpy.int32).reshape((-1, 1)) ).reshape((-1, 3)) + f_colors = numpy.repeat(polygon.mapLineTypeToColor(line_types), 4, 0) - #TODO: Use numpy magic to perform the vertex creation to speed up things. - for i in range(len(points)): - start = points[i - 1] - end = points[i] + builder.addFacesWithColor(f_points, f_indices, f_colors) - normal = normals[i - 1] - - point1 = Vector(data = start - normal) - point2 = Vector(data = start + normal) - point3 = Vector(data = end + normal) - point4 = Vector(data = end - normal) - - builder.addQuad(point1, point2, point3, point4, color = poly_color) - - return builder.build() + + return builder.build() \ No newline at end of file diff --git a/cura/LayerDataBuilder.py b/cura/LayerDataBuilder.py index 7e8e0e636b..2215ed5f27 100644 --- a/cura/LayerDataBuilder.py +++ b/cura/LayerDataBuilder.py @@ -50,16 +50,19 @@ class LayerDataBuilder(MeshBuilder): def build(self): vertex_count = 0 + index_count = 0 for layer, data in self._layers.items(): - vertex_count += data.vertexCount() + vertex_count += data.lineMeshVertexCount() + index_count += data.lineMeshElementCount() vertices = numpy.empty((vertex_count, 3), numpy.float32) colors = numpy.empty((vertex_count, 4), numpy.float32) - indices = numpy.empty((vertex_count, 2), numpy.int32) + indices = numpy.empty((index_count, 2), numpy.int32) - offset = 0 + vertex_offset = 0 + index_offset = 0 for layer, data in self._layers.items(): - offset = data.build(offset, vertices, colors, indices) + ( vertex_offset, index_offset ) = data.build( vertex_offset, index_offset, vertices, colors, indices) self._element_counts[layer] = data.elementCount self.addVertices(vertices) diff --git a/cura/LayerPolygon.py b/cura/LayerPolygon.py index c4dc5d4954..c62113916d 100644 --- a/cura/LayerPolygon.py +++ b/cura/LayerPolygon.py @@ -14,40 +14,113 @@ class LayerPolygon: SupportInfillType = 7 MoveCombingType = 8 MoveRetractionType = 9 - - def __init__(self, mesh, polygon_type, data, line_width): + + __jump_map = numpy.logical_or( numpy.arange(10) == NoneType, numpy.arange(10) >= MoveCombingType ) + + def __init__(self, mesh, extruder, line_types, data, line_widths): self._mesh = mesh - self._type = polygon_type + self._extruder = extruder + self._types = line_types self._data = data - self._line_width = line_width / 1000 - self._begin = 0 - self._end = 0 + self._line_widths = line_widths + + self._vertex_begin = 0 + self._vertex_end = 0 + self._index_begin = 0 + self._index_end = 0 + + self._jump_mask = self.__jump_map[self._types] + self._jump_count = numpy.sum(self._jump_mask) + self._mesh_line_count = len(self._types)-self._jump_count + self._vertex_count = self._mesh_line_count + numpy.sum( self._types[1:] == self._types[:-1]) - self._color = self.__color_map[polygon_type] + # Buffering the colors shouldn't be necessary as it is not + # re-used and can save alot of memory usage. + self._colors = self.__color_map[self._types] + self._color_map = self.__color_map + + # When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType + # Should be generated in better way, not hardcoded. + self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0], dtype=numpy.bool) + + self._build_cache_line_mesh_mask = None + self._build_cache_needed_points = None + + def buildCache(self): + # For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out. + self._build_cache_line_mesh_mask = numpy.logical_not(numpy.logical_or(self._jump_mask, self._types == LayerPolygon.InfillType )) + mesh_line_count = numpy.sum(self._build_cache_line_mesh_mask) + self._index_begin = 0 + self._index_end = mesh_line_count + + self._build_cache_needed_points = numpy.ones((len(self._types), 2), dtype=numpy.bool) + # Only if the type of line segment changes do we need to add an extra vertex to change colors + self._build_cache_needed_points[1:, 0][:, numpy.newaxis] = self._types[1:] != self._types[:-1] + # Mark points as unneeded if they are of types we don't want in the line mesh according to the calculated mask + numpy.logical_and(self._build_cache_needed_points, self._build_cache_line_mesh_mask, self._build_cache_needed_points ) + + self._vertex_begin = 0 + self._vertex_end = numpy.sum( self._build_cache_needed_points ) + - def build(self, offset, vertices, colors, indices): - self._begin = offset - self._end = self._begin + len(self._data) - 1 + def build(self, vertex_offset, index_offset, vertices, colors, indices): + if (self._build_cache_line_mesh_mask is None) or (self._build_cache_needed_points is None ): + self.buildCache() + + line_mesh_mask = self._build_cache_line_mesh_mask + needed_points_list = self._build_cache_needed_points + + # Index to the points we need to represent the line mesh. This is constructed by generating simple + # start and end points for each line. For line segment n these are points n and n+1. Row n reads [n n+1] + # Then then the indices for the points we don't need are thrown away based on the pre-calculated list. + index_list = ( numpy.arange(len(self._types)).reshape((-1, 1)) + numpy.array([[0, 1]]) ).reshape((-1, 1))[needed_points_list.reshape((-1, 1))] + + # The relative values of begin and end indices have already been set in buildCache, so we only need to offset them to the parents offset. + self._vertex_begin += vertex_offset + self._vertex_end += vertex_offset + + # Points are picked based on the index list to get the vertices needed. + vertices[self._vertex_begin:self._vertex_end, :] = self._data[index_list, :] + # 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()] + colors[self._vertex_begin:self._vertex_end, :] *= numpy.array([[0.5, 0.5, 0.5, 1.0]], numpy.float32) - vertices[self._begin:self._end + 1, :] = self._data[:, :] - colors[self._begin:self._end + 1, :] = numpy.array([self._color.r * 0.5, self._color.g * 0.5, self._color.b * 0.5, self._color.a], numpy.float32) + # The relative values of begin and end indices have already been set in buildCache, so we only need to offset them to the parents offset. + self._index_begin += index_offset + self._index_end += index_offset + + indices[self._index_begin:self._index_end, :] = numpy.arange(self._index_end-self._index_begin, dtype=numpy.int32).reshape((-1, 1)) + # When the line type changes the index needs to be increased by 2. + indices[self._index_begin:self._index_end, :] += numpy.cumsum(needed_points_list[line_mesh_mask.ravel(), 0], dtype=numpy.int32).reshape((-1, 1)) + # Each line segment goes from it's starting point p to p+1, offset by the vertex index. + # The -1 is to compensate for the neccecarily True value of needed_points_list[0,0] which causes an unwanted +1 in cumsum above. + indices[self._index_begin:self._index_end, :] += numpy.array([self._vertex_begin - 1, self._vertex_begin]) + + self._build_cache_line_mesh_mask = None + self._build_cache_needed_points = None - for i in range(self._begin, self._end): - indices[i, 0] = i - indices[i, 1] = i + 1 + def getColors(self): + return self._colors - indices[self._end, 0] = self._end - indices[self._end, 1] = self._begin + def mapLineTypeToColor(self, line_types): + return self._color_map[line_types] - def getColor(self): - return self._color + def isInfillOrSkinType(self, line_types): + return self._isInfillOrSkinTypeMap[line_types] - def vertexCount(self): - return len(self._data) + def lineMeshVertexCount(self): + return (self._vertex_end - self._vertex_begin) + + def lineMeshElementCount(self): + return (self._index_end - self._index_begin) @property - def type(self): - return self._type + def extruder(self): + return self._extruder + + @property + def types(self): + return self._types @property def data(self): @@ -55,11 +128,23 @@ class LayerPolygon: @property def elementCount(self): - return ((self._end - self._begin) + 1) * 2 # The range of vertices multiplied by 2 since each vertex is used twice + return (self._index_end - self._index_begin) * 2 # The range of vertices multiplied by 2 since each vertex is used twice @property - def lineWidth(self): - return self._line_width + def lineWidths(self): + return self._line_widths + + @property + def jumpMask(self): + return self._jump_mask + + @property + def meshLineCount(self): + return self._mesh_line_count + + @property + def jumpCount(self): + return self._jump_count # Calculate normals for the entire polygon using numpy. def getNormals(self): @@ -71,7 +156,8 @@ class LayerPolygon: # we end up subtracting each next point from the current, wrapping # around. This gives us the edges from the next point to the current # point. - normals[:] = normals[:] - numpy.roll(normals, -1, axis = 0) + normals = numpy.diff(normals, 1, 0) + # Calculate the length of each edge using standard Pythagoras lengths = numpy.sqrt(normals[:, 0] ** 2 + normals[:, 2] ** 2) # The normal of a 2D vector is equal to its x and y coordinates swapped @@ -85,7 +171,7 @@ class LayerPolygon: return normals - __color_map = { + __color_mapping = { NoneType: Color(1.0, 1.0, 1.0, 1.0), Inset0Type: Color(1.0, 0.0, 0.0, 1.0), InsetXType: Color(0.0, 1.0, 0.0, 1.0), @@ -97,3 +183,17 @@ class LayerPolygon: MoveCombingType: Color(0.0, 0.0, 1.0, 1.0), MoveRetractionType: Color(0.5, 0.5, 1.0, 1.0), } + + # Should be generated in better way, not hardcoded. + __color_map = numpy.array([ + [1.0, 1.0, 1.0, 1.0], + [1.0, 0.0, 0.0, 1.0], + [0.0, 1.0, 0.0, 1.0], + [1.0, 1.0, 0.0, 1.0], + [0.0, 1.0, 1.0, 1.0], + [0.0, 1.0, 1.0, 1.0], + [1.0, 0.74, 0.0, 1.0], + [0.0, 1.0, 1.0, 1.0], + [0.0, 0.0, 1.0, 1.0], + [0.5, 0.5, 1.0, 1.0] + ]) \ No newline at end of file diff --git a/plugins/CuraEngineBackend/Cura.proto b/plugins/CuraEngineBackend/Cura.proto index 5f95a4d4a8..0c4803cc19 100644 --- a/plugins/CuraEngineBackend/Cura.proto +++ b/plugins/CuraEngineBackend/Cura.proto @@ -61,6 +61,28 @@ message Polygon { float line_width = 3; // The width of the line being laid down } +message LayerOptimized { + int32 id = 1; + float height = 2; // Z position + float thickness = 3; // height of a single layer + + repeated PathSegment path_segment = 4; // layer data +} + + +message PathSegment { + int32 extruder = 1; // The extruder used for this path segment + enum PointType { + Point2D = 0; + Point3D = 1; + } + PointType point_type = 2; + 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 +} + + message GCodeLayer { bytes data = 2; } diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index eca6d1fdba..aedc91f130 100644 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -57,6 +57,7 @@ class CuraEngineBackend(Backend): Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) self._onActiveViewChanged() self._stored_layer_data = [] + self._stored_optimized_layer_data = [] #Triggers for when to (re)start slicing: self._global_container_stack = None @@ -77,6 +78,7 @@ class CuraEngineBackend(Backend): #Listeners for receiving messages from the back-end. self._message_handlers["cura.proto.Layer"] = self._onLayerMessage + self._message_handlers["cura.proto.LayerOptimized"] = self._onOptimizedLayerMessage self._message_handlers["cura.proto.Progress"] = self._onProgressMessage self._message_handlers["cura.proto.GCodeLayer"] = self._onGCodeLayerMessage self._message_handlers["cura.proto.GCodePrefix"] = self._onGCodePrefixMessage @@ -139,6 +141,7 @@ class CuraEngineBackend(Backend): self.printDurationMessage.emit(0, [0]) self._stored_layer_data = [] + self._stored_optimized_layer_data = [] if self._slicing: #We were already slicing. Stop the old job. self._terminate() @@ -167,6 +170,7 @@ class CuraEngineBackend(Backend): self._slicing = False self._restart = True self._stored_layer_data = [] + self._stored_optimized_layer_data = [] if self._start_slice_job is not None: self._start_slice_job.cancel() @@ -267,6 +271,12 @@ class CuraEngineBackend(Backend): def _onLayerMessage(self, message): self._stored_layer_data.append(message) + ## Called when an optimized sliced layer data message is received from the engine. + # + # \param message The protobuf message containing sliced layer data. + def _onOptimizedLayerMessage(self, message): + self._stored_optimized_layer_data.append(message) + ## Called when a progress message is received from the engine. # # \param message The protobuf message containing the slicing progress. @@ -284,9 +294,9 @@ class CuraEngineBackend(Backend): self._slicing = False Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time ) if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()): - self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_layer_data) + self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data) self._process_layers_job.start() - self._stored_layer_data = [] + self._stored_optimized_layer_data = [] ## Called when a g-code message is received from the engine. # @@ -357,10 +367,10 @@ class CuraEngineBackend(Backend): 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. - if self._stored_layer_data and not self._slicing: - self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_layer_data) + if self._stored_optimized_layer_data and not self._slicing: + self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data) self._process_layers_job.start() - self._stored_layer_data = [] + self._stored_optimized_layer_data = [] else: self._layer_view_active = False diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index fb1e33366a..c2f73cf5b7 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -15,6 +15,7 @@ from UM.Math.Vector import Vector from cura import LayerDataBuilder from cura import LayerDataDecorator +from cura import LayerPolygon import numpy from time import time @@ -82,26 +83,46 @@ class ProcessSlicedLayersJob(Job): abs_layer_number = layer.id + abs(min_layer_number) 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("polygons")): - polygon = layer.getRepeatedMessage("polygons", p) + for p in range(layer.repeatedMessageCount("path_segment")): + polygon = layer.getRepeatedMessage("path_segment", p) - points = numpy.fromstring(polygon.points, dtype="i8") # Convert bytearray to numpy array - points = points.reshape((-1,2)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. + extruder = polygon.extruder + line_types = numpy.fromstring(polygon.line_type, dtype="u1") # Convert bytearray to numpy array + line_types = line_types.reshape((-1,1)) + + points = numpy.fromstring(polygon.points, dtype="f4") # Convert bytearray to numpy array + if polygon.point_type == 0: # Point2D + points = points.reshape((-1,2)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. + else: # Point3D + points = points.reshape((-1,3)) + + 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. + # 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 # faster. new_points = numpy.empty((len(points), 3), numpy.float32) - new_points[:,0] = points[:,0] - new_points[:,1] = layer.height - new_points[:,2] = -points[:,1] + if polygon.point_type == 0: # Point2D + new_points[:,0] = points[:,0] + new_points[:,1] = layer.height/1000 # layer height value is in backend representation + new_points[:,2] = -points[:,1] + else: # Point3D + new_points[:,0] = points[:,0] + new_points[:,1] = points[:,2] + new_points[:,2] = -points[:,1] + - new_points /= 1000 + this_poly = LayerPolygon.LayerPolygon(layer_data, extruder, line_types, new_points, line_widths) + this_poly.buildCache() + + this_layer.polygons.append(this_poly) - layer_data.addPolygon(abs_layer_number, polygon.type, new_points, polygon.line_width) Job.yieldThread() Job.yieldThread() current_layer += 1 diff --git a/plugins/LayerView/LayerView.py b/plugins/LayerView/LayerView.py index 9725f2292d..bc5ef655b2 100644 --- a/plugins/LayerView/LayerView.py +++ b/plugins/LayerView/LayerView.py @@ -25,6 +25,8 @@ from . import LayerViewProxy from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") +import numpy + ## View used to display g-code paths. class LayerView(View): def __init__(self): @@ -42,7 +44,7 @@ class LayerView(View): self._top_layers_job = None self._activity = False - Preferences.getInstance().addPreference("view/top_layer_count", 1) + Preferences.getInstance().addPreference("view/top_layer_count", 5) Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged) self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count")) @@ -253,11 +255,13 @@ class _CreateTopLayersJob(Job): if not layer or layer.getVertices() is None: continue + layer_mesh.addIndices(layer_mesh._vertex_count+layer.getIndices()) layer_mesh.addVertices(layer.getVertices()) # Scale layer color by a brightness factor based on the current layer number # This will result in a range of 0.5 - 1.0 to multiply colors by. - brightness = (2.0 - (i / self._solid_layers)) / 2.0 + brightness = numpy.ones((1,4), dtype=numpy.float32) * (2.0 - (i / self._solid_layers)) / 2.0 + brightness[0, 3] = 1.0; layer_mesh.addColors(layer.getColors() * brightness) if self._cancel: