A Smooth and Round Voronoi Mesh

I want the edges of a VoronoiMesh to be smooth and round. I have found the following code from this answer

arcgen[{p1_, p2_, p3_}, r_, n_] :=
Module[{dc = Normalize[p1 - p2] + Normalize[p3 - p2], cc, th},
cc = p2 + r dc/EuclideanDistance[dc, Projection[dc, p1 - p2]];
th = Sign[
Det[PadRight[{p1, p2, p3}, {3, 3}, 1]]] (π -
VectorAngle[p3 - p2, p1 - p2])/(n - 1);
NestList[RotationTransform[th, cc],
p2 + Projection[cc - p2, p1 - p2], n - 1]]
roundedPolygon[Polygon[pts_?MatrixQ], r_?NumericQ,
n : (_Integer?Positive) : 12] :=
Polygon[Flatten[
arcgen[#, r, n] & /@
Partition[If[TrueQ[First[pts] == Last[pts]], Most, Identity][pts],
3, 1, {2, -2}], 1]]


Consider for example, the 3x3 hexagonal mesh (see this question for more details)

L1 = 3; L2 = 3;
pts = Flatten[
Table[{3/2 i, Sqrt[3] j + Mod[i, 2] Sqrt[3]/2}, {i, L2 + 4}, {j,
L1 + 4}], 1];
mesh0 = VoronoiMesh[pts];
mesh1 = MeshRegion[MeshCoordinates[mesh0],
With[{a = PropertyValue[{mesh0, 2}, MeshCellMeasure]},
With[{m = 3}, Pick[MeshCells[mesh0, 2], UnitStep[a - m], 0]]]];
mesh = MeshRegion[MeshCoordinates[mesh1],
MeshCells[mesh1, {2, "Interior"}]]


Using roundedPolygon defined above, I can get what I want with

Graphics[{Directive[LightBlue, EdgeForm[Gray], EdgeThickness -> .001],
roundedPolygon[#, 0.3]} & /@ MeshPrimitives[mesh, 2]]


This looks good already, but I have the following questions:

1. Is it possible to fill the gaps between cells automatically? I first thought about setting a Background colour on in Graphics that would match the edge colour. This, however, yields a box look that I want to avoid. I could also change the edge thickness, but this doesn't seem to scale with the lattice size. Any idea how to solve this? The following picture illustrates these cases.

1. Is it possible to scale the EdgeThickness with the mesh size?

2. When I consider a square mesh, given, for example, by pts = Flatten[Table[{i, j}, {i, L2 + 2}, {j, L1 + 2}], 1] and mesh = MeshRegion[MeshCoordinates[mesh0], MeshCells[mesh0, {2, "Interior"}]]

roundedPolygon seems to fail, returning, among others, the error

Any idea how to solve this?

1. Finally, I wonder if it's possible to display the mesh as a mesh-type object and avoid using Graphics.

I don't expect to get an answer to everything, but any ideas or suggestions are welcome.

Edit: The answer to the main problem was already given. However, going a step further, I'm having some troubles using Chip Hurst's code below when considering a random VoronoiMesh. First, it seems that the way diff and joints are defined becomes problematic when considering such type of mesh, different types of errors appear. Furthermore, simply computing the rounded mesh (without filling the spaces), and setting

pts = {RandomReal[L2, L1 L2], RandomReal[L1, L1 L2]} // Transpose;
mesh = VoronoiMesh[pts]


doesn't always yield what I expect from the roundedPolygon option. Occasionaly I get the right rounded mesh

But most times I get wrongly placed polygons

This seems to be an ordering problem, possibly from using Nearest, though I'm not sure. Using Graphics seems to work well with random meshes, but I'd like to be able to work with meshes. Filling the gaps in the random case might get really tricky, but everything works well with either regular square and hexagonal lattices, just wondering if we could go a step further. Any ideas?

• What about putting just a colored disk with appropriate radius under each interior vertex of the grid? Or you put a copy of your grid with increased EdgeForm[Thickness[whateveryouwant] below. – Henrik Schumacher Jan 18 '20 at 14:24
• Good idea! I think that is more or less what @kglr is doing below. Just wondering how I should set the correct scaling of their radii, depending on the mesh dimensions. – sam wolfe Jan 18 '20 at 15:51

1 + 4

We can discretize the rounded Polygon objects and then add the negative of the mesh through Prolog.

rm = DiscretizeGraphics[roundedPolygon[#, 0.3] & /@ MeshPrimitives[mesh, 2]]


Now there's some floating point differences in the results from roundedPolygon that seem to effect subsequent Boolean operations. We can fix this crudely merging nearby points.

coordsnew = Mean /@ Nearest[MeshCoordinates[rm], MeshCoordinates[rm], {All, 10^-12.}];
rm = MeshRegion[coordsnew, MeshCells[rm, 2]];


Now find the difference:

diff = BoundaryMesh @ RegionDifference[
Cuboid @@ Transpose[CoordinateBounds[MeshCoordinates[rm], Scaled[.05]]], rm]


And assemble:

joints = With[{comps = ConnectedMeshComponents[diff]},
If[Length[comps] == 1,
{},
Show[
BoundaryMeshRegion[
RegionUnion[Rest[SortBy[comps, RegionBounds]]],
MeshCellStyle -> {1 -> None, 2 -> GrayLevel[.3]}]
][[1]]
]
];

MeshRegion[
rm,
MeshCellStyle -> {1 -> {Thick, GrayLevel[.3]}, 2 -> LightBlue},
Prolog -> joints
]


3

It seems roundedPolygon is effected by an unnecessary run of consecutive duplicate points. We can fix this by deleting them.

roundedPolygon[p:Polygon[_?MatrixQ], zero_?PossibleZeroQ, ___] := p

roundedPolygon[Polygon[opts_?MatrixQ], r_?Positive, n : (_Integer?Positive) : 12] :=
With[{pts = Split[opts][[All, 1]]},
Polygon[Flatten[arcgen[#, r, n] & /@
Partition[
If[TrueQ[First[pts] == Last[pts]], Most, Identity][pts],
3, 1, {2, -2}
], 1]]
]


Edit

We can use MeshCellShapeFunction to preserve the data in the original mesh while having custom rounded cells:

meshsty = MeshRegion[
mesh,
MeshCellShapeFunction -> {2 -> (roundedPolygon[Polygon[#], 0.3]&)},
MeshCellStyle -> {1 -> {Thick, GrayLevel[.3]}, 2 -> LightBlue},
Epilog -> joints
]


Notice that this only the visualization is affected and not the underlying data:

RegionEqual[mesh, meshsty]


True

Whereas the original solution does change the underlying data:

RegionEqual[mesh, rm]


False

• See my edit. ${}$ – Chip Hurst Jan 19 '20 at 15:43
• Thank you! This is great. – sam wolfe Jan 19 '20 at 15:44
• Sorry, just one more thing. I'm still getting the same error when r==0. It would be nice to have this option and get the "unrounded" mesh for r=0. – sam wolfe Jan 19 '20 at 23:48
• See my edit. I think this does what you want. Make sure to run Clear[roundedPolygon] beforehand. – Chip Hurst Jan 19 '20 at 23:51
• Chip, thanks for fixing my routine; I certainly did not design it for the case where there are repeated points between the first and last one. @sam, "it's not always possible to get the intended smooth corners" - for a sufficiently small polygon, you also need a sufficiently small rounding radius (second argument of roundedPolygon[]), and I did not provide for an automatic cutoff. You could use Manipulate[] to help explore a good rounding radius value, and then process tiny polygons (perhaps filtered by area or perimeter?) separately. – J. M.'s ennui Jan 22 '20 at 4:11
1. fill the gaps between cells
Graphics[{PointSize[1 / L2 / 3], Red, MeshPrimitives[mesh, {0, "Interior"}],
{Directive[LightBlue, EdgeForm[Gray], EdgeThickness -> .001],
roundedPolygon[#, 0.3]} & /@ MeshPrimitives[mesh, 2]}]


• For bigger mesh sizes (L1=L2=20, for example), the AbsolutePointSize value is too big and the dots overflow the mesh. So maybe should value should scale with L1 and L2. What would be the best way to do this? Maybe the same reasoning could be extended to the EdgeThickness (question 2). – sam wolfe Jan 18 '20 at 15:46
• @samwolfe, changing AbsolutePointSize[30] to PointSize[1/L2/3] seems to work. – kglr Jan 18 '20 at 17:42