# Feature Request: 3D Honeycomb infill #1646

Closed
opened this Issue Dec 28, 2013 · 35 comments

Projects
None yet
Contributor

### gringer commented Dec 28, 2013

 The current infill patterns (apart from concentric) seem to be 2D in nature, creating vertical walls. It would be nice to have different patterns where the fill pattern for each layer is dependent on the Z direction / height, allowing for stronger support structures with less infill. I have an example of one such pattern here, which uses a tesselation of a truncated octahedron: http://www.thingiverse.com/thing:214169 There are other examples of space-filling tesselations: http://en.wikipedia.org/wiki/Honeycomb_%28geometry%29#Space-filling_polyhedra.5B2.5D
Contributor

### gringer commented Dec 30, 2013

 I've added an octagon/tetrahedron tesselation to the thingiverse page as well. Here's my rough description of the tesselations, sort of as a 3D printer would view them going from bottom to top (as discovered by investigating my models in Repetier Host with the 'Cut Objects' function): Tetrahedron/Octahedron: Start from a regular square grid, form square on one diagonal and expand it from the corner, pushing the "old" square smaller to eventually return to a regular square grid. Truncated tetrahedron: Start from a regular square grid and expand from centre (truncating diagonals to form diamonds) until a regular octahedron is formed, then reduce from centre to return to the square grid and continue reducing to form an octahedron in the other direction, then expand back to the square grid. Although the truncated tetrahedron is a more geometrically complex structure, it would probably work better on a 3D printer because it can be done with relative filament travel angles that are no steeper than 45 degrees (excluding edges). The octahedron structure requires sharper 90 degree turns for printing. In order to form the pattern for the truncated tetrahedron you would travel vertically doing zigzags as necessary, then turn 180 degrees at the end to do the next square over in the horizontal direction. Once reaching the end of the pattern in the horizontal direction, you rotate 90 degrees and proceed in a similar way in the vertical direction. This will create walls that are only one filament width thick, so it might be better to go forward and back along a path before shifting over to create a 2-filament thick wall. Edit: an image to demonstrate this is attached. The grids are twice magnified in the non-square grid views because I got tired of making mistakes. The rectangles should be squares, but I had trouble getting that working with the grid I was using for laying out the points.
Contributor

### gringer commented Jan 9, 2014

 I've now written an R script to generate path points at a given Z height, assuming a grid size of 1x1 unit. For the purpose of making the lines easier to see, I've coloured the horizontal-ish lines blue and vertical-ish lines red. Most of the script complexity is involved in trying to deal with the edges of the pattern. An animated GIF generated from this is attached: ```#!/usr/bin/Rscript for(zHeight in (0:39 / (10*sqrt(2)))){ png(sprintf("Rplot_z%0.3f.png",zHeight)); octagramGap = 1 / (1 + sqrt(2)); # offset required to create a regular octagram # sawtooth wave function, assuming edge length 1, height 4/sqrt(2) gap = (abs(((zHeight * sqrt(2)) %% 4) - 2) - 1) * octagramGap; gridHeight = 9; # number of cells in Y direction gridWidth = 5; # number of cells in X direction plot(-2.5:7.5, -0.5:9.5, type = "n", xlab = paste("Grid width:",gridWidth), ylab = paste("Grid height:",gridHeight), main = sprintf("Truncated Octagram Space Filling Tesselation\n[Height = %0.3f]",zHeight)); gapSepLine = c(-abs(gap)/2, abs(gap)/2); # offset along the direction of travel gapSepOffset = c(-gap/2, gap/2); # offset perpend to the direction of travel columnPoints = cbind(rep(0,gridHeight+1),0:gridHeight); for (xOffset in 1:(gridWidth-1)){ yPointBase = c(rep(1:gridHeight, each = 2)); yPoints = c(0,abs(gap)/2,head(yPointBase + gapSepLine,-1),gridHeight); xPoints = c(0,head(rep(gapSepOffset, gridHeight, each = 2), gridHeight*2),0) * (-1)^xOffset + xOffset; pointsToAdd = cbind(xPoints, yPoints); if(xOffset %% 2 == 1){ pointsToAdd = cbind(rev(xPoints),rev(yPoints)); } columnPoints = rbind(columnPoints, pointsToAdd); } if(gridWidth %% 2 == 0){ columnPoints = rbind(columnPoints, cbind(rep(gridWidth,gridHeight+1),0:gridHeight)); } else { columnPoints = rbind(columnPoints, cbind(rep(gridWidth,gridHeight+1),rev(0:gridHeight))); } points(columnPoints, type = "l", col = "red", lwd = 4); rowPoints = cbind(0:gridWidth, rep(0,gridWidth+1)); for(yOffset in 1:(gridHeight-1)){ xPointBase = c(rep(1:gridWidth, each = 2)); xPoints = c(0,abs(gap)/2,head(xPointBase + gapSepLine,-1),gridWidth); yPoints = c(0,head(rep(gapSepOffset, gridWidth, each = 2), gridWidth*2),0) * (-1)^(yOffset) + yOffset; pointsToAdd = cbind(xPoints, yPoints); if(yOffset %% 2 == 1){ pointsToAdd = cbind(rev(xPoints),rev(yPoints)); } rowPoints = rbind(rowPoints, pointsToAdd); } if(gridWidth %% 2 == 0){ rowPoints = rbind(rowPoints, cbind(0:gridWidth,rep(gridHeight,gridWidth+1))); } else { rowPoints = rbind(rowPoints, cbind(rev(0:gridWidth),rep(gridHeight,gridWidth+1))); } points(rowPoints, type = "l", col = "blue", lwd = 2); ##Sys.sleep(0.2); graphics.off(); }```
Member

### alexrj commented Jan 9, 2014

 Hello! Is there any chance you can provide that implemented in C or C++ or Perl? It should just be a function that takes a Z height and returns a set of polylines (or closed polygons, as you like) for that height. Thank you for contributing :)
Member

### alexrj commented Jan 9, 2014

 BTW, your animations show that two lines overlap at every diagonal edge. That's not ideal for 3D printing because it means that several jumps are needed, and extrusion must stop and start lots of times. If you look closely at how honeycomb infill is implemented, you'll notice that those lines are duplicated and slightly moved so that two adjacent diagonal traces can be extruded continuously. I recommend you take the same approach.
Member

### hroncok commented Jan 9, 2014

 This looks great 👍
Contributor

### gringer commented Jan 9, 2014

 Yes, I can convert this to Perl. I'll have a go today. For the overlap I'd probably just do something like shift the vertical lines half a width down, and the horizontal lines half a width up. There will still be dots of overlap at the grid intersections -- hopefully that won't be a problem.
Contributor

### gringer commented Jan 10, 2014

 Okay, here it is, as a Perl script, with some example SVG output that I used for debugging. I've shifted the vertical line points down, but not modified the horizontal points. I also made it so that the lines are not connected, because it sounded from your request that unconnected lines might be better for you: ```#!/usr/bin/perl =head1 DESCRIPTION Creates a contiguous sequence of points at a specified height that make up a horizontal slice of the edges of a space filling truncated octahedron tesselation. The octahedrons are oriented so that the square faces are in the horizontal plane with edges parallel to the X and Y axes. =cut use warnings; use strict; use POSIX "fmod"; =head1 FUNCTIONS =cut =head2 colinearPoints(offset, gridLength) Generate an array of points that are in the same direction as the basic printing line (i.e. Y points for columns, X points for rows) Note: a negative offset only causes a change in the perpendicular direction =cut sub colinearPoints{ my (\$offset, \$gridLength) = @_; my @points = (); for(my \$i = 0; \$i < \$gridLength; \$i++){ push(@points, \$i); push(@points, \$i + abs(\$offset/2)); push(@points, (\$i+1) - abs(\$offset/2)); } push(@points, \$gridLength); return(@points); } =head2 colinearPoints(offset, baseLocation, gridLength) Generate an array of points for the dimension that is perpendicular to the basic printing line (i.e. X points for columns, Y points for rows) =cut sub perpendPoints{ my (\$offset, \$baseLocation, \$gridLength) = @_; my @points = (); for(my \$i = 0; \$i < \$gridLength; \$i++){ my \$side = (2*((\$i+\$baseLocation) % 2) - 1); push(@points, \$baseLocation); push(@points, \$baseLocation + \$offset/2 * \$side); push(@points, \$baseLocation + \$offset/2 * \$side); } push(@points, \$baseLocation); return(@points); } =head2 trim(pointArrayRef, minX, minY, maxX, maxY) Trims an array of points to specified rectangular limits. Point components that are outside these limits are set to the limits. =cut sub trim{ my (\$pointArrayRef, \$minX, \$minY, \$maxX, \$maxY) = @_; foreach (@{\$pointArrayRef}){ \$_->[0] = (\$_->[0] < \$minX) ? \$minX : ((\$_->[0] > \$maxX) ? \$maxX : \$_->[0]); \$_->[1] = (\$_->[1] < \$minY) ? \$minY : ((\$_->[1] > \$maxY) ? \$maxY : \$_->[1]); } } =head2 makeNormalisedGrid(z, gridWidth, gridHeight, lineSeparation) Generate a set of curves (array of array of 2d points) that describe a horizontal slice of a truncated regular octahedron with edge length 1. =cut sub makeNormalisedGrid{ my (\$z, \$gridWidth, \$gridHeight, \$lineSeparation) = @_; # offset required to create a regular octagram my \$octagramGap = 1 / (1 + sqrt(2)); # sawtooth wave function for range f(\$z) = [-\$octagramGap .. \$octagramGap] my \$offset = (abs((fmod(\$z * sqrt(2), 4)) - 2) - 1) * \$octagramGap; my @points = (); for(my \$x = 0; \$x <= \$gridWidth; \$x++){ my @xPoints = perpendPoints(\$offset, \$x, \$gridHeight); my @yPoints = colinearPoints(\$offset, \$gridHeight); # This is essentially @newPoints = zip(@xPoints, @yPoints) my @newPoints = map { [(\$xPoints[\$_],\$yPoints[\$_] - \$lineSeparation)] } (0 .. \$#xPoints); # trim points to grid edges trim(\@newPoints,0,0,\$gridWidth,\$gridHeight); if((\$x + \$gridWidth) % 2 == 0){ push(@points, [ @newPoints ]); } else { push(@points, [ reverse(@newPoints) ]); } } for(my \$y = 0; \$y <= \$gridHeight; \$y++){ my @xPoints = colinearPoints(\$offset, \$gridWidth); my @yPoints = perpendPoints(\$offset, \$y, \$gridWidth); my @newPoints = map { [(\$xPoints[\$_],\$yPoints[\$_])] } (0 .. \$#xPoints); # trim points to grid edges trim(\@newPoints,0,0,\$gridWidth,\$gridHeight); if(\$y % 2 == 0){ push(@points, [ @newPoints ]); } else { push(@points, [ reverse(@newPoints) ]); } } return @points; } =head2 makeGrid(z, gridSize, gridWidth, gridHeight, lineSeparation) Generate a set of curves (array of array of 2d points) that describe a horizontal slice of a truncated regular octahedron with a specified grid square size. =cut sub makeGrid{ my (\$z, \$gridSize, \$gridWidth, \$gridHeight, \$lineSeparation) = @_; my \$scaleFactor = \$gridSize; my \$normalisedZ = \$z / \$scaleFactor; my \$normalisedSep = \$lineSeparation / \$scaleFactor; my @points = makeNormalisedGrid(\$normalisedZ, \$gridWidth, \$gridHeight, \$normalisedSep); foreach my \$lineRef (@points){ foreach my \$pointRef (@{\$lineRef}){ \$pointRef->[0] *= \$scaleFactor; \$pointRef->[1] *= \$scaleFactor; } } return @points; } # example -- SVG output my \$z = 0; print < EOT```
Member

### alexrj commented Jan 10, 2014

 Wow, thank you @gringer. Nicely coded too. Perhaps you should add an argument to `makeGrid()` for passing the angle (0°/90°)? It seems to me that `makeGrid()` currently returns twice the paths needed for a certain layer. Or maybe I'm getting this wrong, but looking at the exported SVG there are some paths crossing other paths, which is generally bad:
Contributor

### gringer commented Jan 10, 2014

 Or maybe I'm getting this wrong, but looking at the exported SVG there are some paths crossing other paths, which is generally bad The makeGrid() function generates sets of two types of curves, horizontally oriented curves and vertically oriented curves, but the curves are not joined to each other. Paths do cross, but do not intersect except at single points (which is unavoidable if you want to make sure that no angles along the extrusion path, except for edges, are 90°). Note that I've clipped the lines at the edges of the grid, which may make it appear that the edge lines overlap. The separate line segments are a bit easier to see on the SVG if you split it up into component paths and randomly colour each path: If this edge clipping method is not desirable, I could remove the clipped line segments entirely (which would leave gaps in the outer curves), or adjust the pattern location/width so that the octagram edges line up exactly with the clip region. I would imagine that this pattern would be implemented by generating a pattern that covers a little bit more than the bounding box of the model, intersecting it with the islands of the Z-slice, and only printing pattern lines where the pattern lies within an island, so the precise implementation of the edges shouldn't be too much of an issue.
Contributor

### gringer commented Jan 10, 2014

 If paths crossing at grid intersections are completely undesirable, then there are two options for fixing that: Allow angles of 90° in the pattern. This will probably slow down the pattern generation by quite a lot and make the path definition a bit more complicated, so I'd rather not do this. Alternate between column and row curves on each layer. This will mean that the diagonal edges will have a single thickness, and alternating horizontal/vertical lines will be missing on the square components.
Contributor

### gringer commented Jan 11, 2014

 Okay, I've implemented option 2, which alternates between horizontal and vertical lines for each layer and has no offset lines. The example output of the script is now a javascript-animated SVG file to demonstrate this (view in your web browser) -- the current layer is a black line, and the next layer is a transparent grey line. The grid now extends beyond the specified limits (which I've indicated with a yellow rectangle in the example SVG output), with the assumption that lines will be clipped to the limits as necessary: The final value in makeGrid defines which of columns/rows to print. 1 = columns, 2 = rows, 3 = both. I think that was what you were requesting, but I'm not completely sure. ```#!/usr/bin/perl =head1 DESCRIPTION Creates a contiguous sequence of points at a specified height that make up a horizontal slice of the edges of a space filling truncated octahedron tesselation. The octahedrons are oriented so that the square faces are in the horizontal plane with edges parallel to the X and Y axes. =cut use warnings; use strict; use POSIX "fmod"; =head1 FUNCTIONS =cut =head2 colinearPoints(offset, gridLength) Generate an array of points that are in the same direction as the basic printing line (i.e. Y points for columns, X points for rows) Note: a negative offset only causes a change in the perpendicular direction =cut sub colinearPoints{ my (\$offset, \$baseLocation, \$gridLength) = @_; my @points = (); push(@points, \$baseLocation - abs(\$offset/2)); for(my \$i = 0; \$i < \$gridLength; \$i++){ push(@points, \$baseLocation + \$i + abs(\$offset/2)); push(@points, \$baseLocation + (\$i+1) - abs(\$offset/2)); } push(@points, \$baseLocation + \$gridLength + abs(\$offset/2)); return(@points); } =head2 colinearPoints(offset, baseLocation, gridLength) Generate an array of points for the dimension that is perpendicular to the basic printing line (i.e. X points for columns, Y points for rows) =cut sub perpendPoints{ my (\$offset, \$baseLocation, \$gridLength) = @_; my @points = (); my \$side = (2*((\$baseLocation) % 2) - 1); push(@points, \$baseLocation - \$offset/2 * \$side); for(my \$i = 0; \$i < \$gridLength; \$i++){ \$side = (2*((\$i+\$baseLocation) % 2) - 1); push(@points, \$baseLocation + \$offset/2 * \$side); push(@points, \$baseLocation + \$offset/2 * \$side); } push(@points, \$baseLocation - \$offset/2 * \$side); return(@points); } =head2 trim(pointArrayRef, minX, minY, maxX, maxY) Trims an array of points to specified rectangular limits. Point components that are outside these limits are set to the limits. =cut sub trim{ my (\$pointArrayRef, \$minX, \$minY, \$maxX, \$maxY) = @_; foreach (@{\$pointArrayRef}){ \$_->[0] = (\$_->[0] < \$minX) ? \$minX : ((\$_->[0] > \$maxX) ? \$maxX : \$_->[0]); \$_->[1] = (\$_->[1] < \$minY) ? \$minY : ((\$_->[1] > \$maxY) ? \$maxY : \$_->[1]); } } =head2 makeNormalisedGrid(z, gridWidth, gridHeight, curveType) Generate a set of curves (array of array of 2d points) that describe a horizontal slice of a truncated regular octahedron with edge length 1. curveType specifies which lines to print, 1 for vertical lines (columns), 2 for horizontal lines (rows), and 3 for both. =cut sub makeNormalisedGrid{ my (\$z, \$gridWidth, \$gridHeight, \$curveType) = @_; # offset required to create a regular octagram my \$octagramGap = 1 / (1 + sqrt(2)); # sawtooth wave function for range f(\$z) = [-\$octagramGap .. \$octagramGap] my \$offset = (abs((fmod(\$z * sqrt(2), 4)) - 2) - 1) * \$octagramGap; my @points = (); if((\$curveType & 1) != 0){ for(my \$x = 0; \$x <= \$gridWidth; \$x++){ my @xPoints = perpendPoints(\$offset, \$x, \$gridHeight); my @yPoints = colinearPoints(\$offset, 0, \$gridHeight); # This is essentially @newPoints = zip(@xPoints, @yPoints) my @newPoints = map { [(\$xPoints[\$_], \$yPoints[\$_])] } (0 .. \$#xPoints); # trim points to grid edges #trim(\@newPoints,0,0,\$gridWidth,\$gridHeight); if(\$x % 2 == 0){ push(@points, [ @newPoints ]); } else { push(@points, [ reverse(@newPoints) ]); } } } if((\$curveType & 2) != 0){ for(my \$y = 0; \$y <= \$gridHeight; \$y++){ my @xPoints = colinearPoints(\$offset, 0, \$gridWidth); my @yPoints = perpendPoints(\$offset, \$y, \$gridWidth); my @newPoints = map { [(\$xPoints[\$_],\$yPoints[\$_])] } (0 .. \$#xPoints); # trim points to grid edges #trim(\@newPoints,0,0,\$gridWidth,\$gridHeight); if(\$y % 2 == 0){ push(@points, [ @newPoints ]); } else { push(@points, [ reverse(@newPoints) ]); } } } return @points; } =head2 makeGrid(z, gridSize, gridWidth, gridHeight, curveType) Generate a set of curves (array of array of 2d points) that describe a horizontal slice of a truncated regular octahedron with a specified grid square size. =cut sub makeGrid{ my (\$z, \$gridSize, \$gridWidth, \$gridHeight, \$curveType) = @_; my \$scaleFactor = \$gridSize; my \$normalisedZ = \$z / \$scaleFactor; my @points = makeNormalisedGrid(\$normalisedZ, \$gridWidth, \$gridHeight, \$curveType); foreach my \$lineRef (@points){ foreach my \$pointRef (@{\$lineRef}){ \$pointRef->[0] *= \$scaleFactor; \$pointRef->[1] *= \$scaleFactor; } } return @points; } # example -- animated SVG output # 40 horizontal slices, grid of size 10mm per cell, 5 cells wide by 8 high my \$numSlices = 40; my \$gridSize = 10; my \$gridWidth = 5; my \$gridHeight = 8; my \$gridMaxX = \$gridSize * (\$gridWidth); my \$gridMaxY = \$gridSize * (\$gridHeight); my \$svgMinX = -\$gridSize/2; my \$svgMinY = -\$gridSize/2; my \$svgWidth = \$gridSize * (\$gridWidth+1); my \$svgHeight = \$gridSize * (\$gridHeight+1); my \$lineWidth = \$gridSize / 20; print < EOT```
Member

### alexrj commented Jan 11, 2014

 I really appreciate your contribution. I will implement that as soon as possible.
Contributor

### gringer commented Jan 13, 2014

 FWIW, here's one version of printing all the squares (with one side doubled) in a single layer. It doubles back on its own path for every second octagonal edge, which will produce extrusion blobs at these locations due to extruder hysteresis and the extra time required for a complete reversal of printing direction:

### traveltrousers commented Jun 26, 2014

 it's in for implementation, lets hope it works out, looks great
Contributor

### gringer commented Jul 5, 2014

 This version has solid walls for each face, but someone had a go at printing the wireframe version using my STL file: http://www.thingiverse.com/make:81810
Member

### alexrj commented Jul 9, 2014

 Not abandoned at all, I will merge it soon - sorry, I've been working on other things!

### alexrj added a commit that referenced this issue Jul 26, 2014

``` New 3D Honeycomb infill pattern (credits: David Eccles (gringer)). #1646 ```
``` 53f2d6b ```
Member

### alexrj commented Jul 26, 2014

 Good news! I merged this code. Thank you very much @gringer. Here's a preview:

### misan commented Jul 26, 2014

 It looks great.

### jiripech commented Jul 27, 2014

 Oh, that's so cool! Thank you, guys!

### trbielec commented Jul 28, 2014

 Fantastic work! This looks incredible!
Contributor

### gringer commented Jul 28, 2014

 Excellent, thanks. It looks like it might be a bit tall (unless you were going for that effect) -- the diamonds on the outside should be squares, and each non-diamond face should be a regular hexagon. However, a taller version shouldn't be a problem and might be better because the angle is steeper.
Member

### alexrj commented Jul 29, 2014

 I named this "3D honeycomb". Would "Octahedrons (3D)" be better?

### whosawhatsis commented Jul 29, 2014

 It would certainly be more accurate, which I'm all for.
Contributor

### gringer commented Jul 29, 2014

 A honeycomb is any space-filling tesselation. A rectangular grid with a full layer every n layers would also be a called a honeycomb in the geometric sense. https://en.wikipedia.org/wiki/Honeycomb_%28geometry%29 If you want to be specific, this is a "bitruncated cubic honeycomb", although "Octahedrons (3D)" is probably going to better describe what people see when it's printing. https://en.wikipedia.org/wiki/Bitruncated_cubic_honeycomb
Member

### alexrj commented Aug 3, 2014

 Then I think I'll leave "Honeycomb (3D)", since it's not wrong and octahedrons is less correct (we have truncated octahedrons). Also, the 2D honeycomb is not called hexagons...

Member

### alexrj commented Aug 8, 2014

 Uhm, I think there's something wrong with Z scaling somewhere. The truncated octahedrons are stretched vertically. This is visible in my screenshot above where the squares look like rhomboids...

Member

### alexrj commented Aug 8, 2014

 By the way, when changing `\$octagramGap` to 1 I get a much more interesting tessellation… it's not made from truncated octahedrons anymore, but it's nice looking and also it provides completely closed cells instead of vertical voids:

### alexrj added a commit that referenced this issue Aug 8, 2014

``` Fix truncated octahedrons. #1646 ```
``` 45fc748 ```
Member

### alexrj commented Aug 8, 2014

 @gringer, how can we try to fix the formulas in order to get regular truncated octahedrons? This is the closest I was able to do: ```my \$octagramGap = 3/4; my \$wave = abs(fmod(\$z, 2) - 1)*2 - 1; my \$offset = \$wave * \$octagramGap;``` This way the resulting solid has square faces but no hexagons yet, resulting in a nicer honeycomb by the way:

### alexrj added a commit that referenced this issue Aug 8, 2014

``` Fix truncated octahedrons. #1646 ```
``` 43b1aab ```
Member

### alexrj commented Aug 8, 2014

 Okay. Got it: ```my \$octagramGap = 0.5; my \$a = sqrt(2); # period my \$wave = abs(fmod(\$z, \$a) - \$a/2)/\$a*4 - 1; my \$offset = \$wave * \$octagramGap;```

### trbielec commented Aug 8, 2014

 Closed cells is exactly what I was looking for. Thank you @alexrj! Awesome work!
Contributor

### gringer commented Aug 8, 2014

 Great, that looks correct now. Sorry I couldn't help out with the recent debugging.

### Recmo commented Aug 11, 2014

 It's probably not worth the extra complexity, but the Weiare-Phelan structure is supposed to be slightly more efficient. (See https://en.wikipedia.org/wiki/Weaire%E2%80%93Phelan_structure and http://www.nytimes.com/imagepages/2008/08/05/science/20080805_SWIM_GRAPHIC.html). Also, Is it possible to complete the octahedrons by filling (bridging) the square when it is at its minimal size? Or perhaps twice in orthogonal directions.
Member

### alexrj commented Nov 8, 2014

 @Recmo, that infill pattern looks interesting. I'm afraid I have not enough time for studying how to implement it though at the moments, so I accept any help. Regarding filling the squares, I'm afraid that would cause lots of small bridges with poor anchors and lots of retractions for moving between them. But I understand the usefulness of closed cells...

Closed

Closed

Closed

Member

### lordofhyphens commented Jun 18, 2016

 Yeah, it's done.