Merge branch 'layerview_dev' of https://github.com/Johan3DV/Cura into Johan3DV-layerview_dev

This commit is contained in:
Tim Kuipers 2016-07-25 16:41:13 +02:00
commit c6e9e62e73
7 changed files with 264 additions and 85 deletions

View file

@ -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()

View file

@ -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)

View file

@ -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]
])

View file

@ -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;
}

View file

@ -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

View file

@ -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

View file

@ -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: