classy_blocks tutorial part 8: Custom Sketches

This is the 8th part of theĀ classy_blocks tutorial. If you’re new to this, I recommend to scroll through previous parts first.

A classy_blocks Sketch

This is a collection of Faces but instead of creating each face manually, you just need to specify a bunch of points and which ones define which Face. Let’s create a square with rounded corners for a demonstration. Note that this shape needs a bit more sophisticated blocking scheme, something like this – gray numbers indicate points, block numbers are blue:

It is a perfectly fine plan to create a new RoundSquare class that inherits from Sketch and create all Face objects manually. But it is also a very clumsy way so let’s not do that. Instead, we’ll only define outer points and their relations with faces. Instead of inheriting from a plain Sketch we’ll use MappedSketch that will handle face creation for us.

Quadrangle Map

First, each face is defined by 4 points a.k.a. quadrangle, hence quads:

import classy_blocks as cb
import numpy as np
from classy_blocks.types import PointType
from classy_blocks.util import functions as f

class RoundSquare(cb.MappedSketch):
    quads = [
        [20, 0, 1, 12],
        [12, 1, 2, 13],
        [13, 2, 3, 21],
        [21, 20, 12, 13],
        [21, 3, 4, 14],
        [14, 4, 5, 15],
        [15, 5, 6, 22],
        [22, 21, 14, 15],
        [22, 6, 7, 16],
        [16, 7, 8, 17],
        [17, 8, 9, 23],
        [23, 22, 16, 17],
        [23, 9, 10, 18],
        [18, 10, 11, 19],
        [19, 11, 0, 20],
        [20, 23, 18, 19],
        [21, 22, 23, 20],
    ]

Note how all quads follow the same convention – outer edges are always from point 2 to point 3. We’ll get to that soon.

Point List

Now we need to place outer points from 0 through 11. It is a short and simple script because we’ll hard-code most stuff for now – sizing, positions etc. We’ll just calculate 3 points and revolve them 4 times. These points will be calculated when our custom sketch is initialized.

    def __init__(self, center: PointType, side: float, corner_round: float):
        center = np.array(center)
        points = [
            center + f.vector(side / 2, 0, 0),
            center + f.vector(side / 2, side / 2 - side * corner_round / 2, 0),
            center + f.vector(side / 2 - side * corner_round / 2, side / 2, 0),
        ]

        outer_points = []

        angles = np.linspace(0, 2 * np.pi, num=4, endpoint=False)
        for a in angles:
            for i in range(3):
                outer_points.append(f.rotate(points[i], a, [0, 0, 1], center))

Next, we’re missing inner points which we could manually calculate right away. That would mean calculating 3 more points and rotating them 4 more times. But we don’t know where should we put them – a little experimenting would probably enlighten us promptly but there’s a better way – we’ll throw them all in the center:

        inner_points = np.ones((12, 3)) * center

        super().__init__(np.concatenate((outer_points, inner_points), axis=0), RoundSquare.quads)

We can now initialize our sketch with base = RoundSquare([0, 0, 0], 1, 0.5) but it will be invalid because we haven’t calculated our internal points yet. If we try to make a blockMesh from it, it will fail and a quick look into debug.vtk will show us why:

Smoothing

To place inner points we can use Laplacian smoothing. It will place each point to an average location of their neighbours. This will set a good enough position for them with no additional rules or calculations.

A note on Laplacian smoothing: Sketches that have sharp corners and especially ones that have concave features will probably get worse with smoothing. A simple average of positions can as well produce a position outside of sketch, rendering it useless for anything.

Fortunately for us, in this example the sketch is completely convex which means our sketch will be just fine after we smooth it out. It’s a very simple thing to do with classy_blocks:

smoother = cb.SketchSmoother(base)
smoother.smooth()

And voila, the shape is there!

It’s a pretty one but doesn’t really have rounded edges. So let’s make them.

Curved edges

The MappedSketch has a method called add_edges() which is called in __init__(). It doesn’t do anything unless you tell it what to do. We’ll use it to create edges on faces 1, 5, 9, and 13. All those quads have outer edge between 2nd and 3rd point (what a stroke of luck!) so the code will be pretty simple. In this case, the cb.Angle() definition will be the best. We know it’s a 90-degree arc and we also know its axis (which is the same as sketch normal).

    def add_edges(self) -> None:
        for i in (1, 5, 9, 13):
            self.faces[i].add_edge(1, cb.Angle(np.pi / 2, [0, 0, 1]))

Easy peasy!

Manual Chopping

Note that I explicitly state manual chopping because there’s automatic grading in development. Stay tuned for a few more decades until I manage to whip it up! But until then we’ll have to specify what to chop and how.

Also note that the above screenshots are dumb chops created using this horrible piece of code:

for operation in shape.operations:
    for i in range(3):
        operation.chop(i, count=10)

I’ll be angry if I see you doing that so don’t try that at home.

Instead, we can specify what to chop on sketch itself and whatever Shape we’ll create from it will know what to do. It’s always wise to chop as little blocks as possible and let the automatic propagation do its job instead. This way we will avoid inconsistent counts and gradings.

We have to select the right blocks that will define all other ones and we have to do that for two directions. Specifically for this sketch, we’ll do:

  • Direction 0:
    • Quad 0 will define all outer quads, that is 0 through 14;
  • Direction 1:
    • Quad 0 will define 0, 3, and 2
    • Quad 1 will define 1, 3, 16, 11, and 9
    • Quads 4, 8 and 12 will define their respective corner buddies.

Now we know how this should be done but we don’t have to write for loops to cut specific Operations in each Shape. We simply define a chops class variable:

    chops = [[0], [0, 1, 4, 5, 8, 12]]

So now we can simply do Shape.chop(direction, ...) as with a normal Operation such as Extrude, Loft etc. Of course we haven’t defined chops for direction 2 but the Shape itself knows which direction is that.

shape = cb.ExtrudedShape(base, 1)
shape.chop(0, start_size=0.05)
shape.chop(1, start_size=0.05)
shape.chop(2, count=5)

Customizing Customizations

Patches

There’s not much more to be said about custom sketches and shapes, created from them. The result is a Sketch that has the .operations property – they follow the same order as the quads you supplied in the class. To do further business with your shape, you have to refer to those. For instance, if you wanted to set side patches to walls, you’d do something like that:

for operation in shape.operations[:15]:
    operation.set_patch("right", "sides")

mesh.modify_patch("sides", "wall")

Optimization vs. Smoothing

Smoothing only does what I described above – move points to the average of their neighbours. It’s much too often the case that smoothing makes things even worse or at least far from the best configuration. In those cases, Optimization will do better. However, optimizers can’t start from an invalid sketch (such as ours before smoothing) so you have to provide a good starting point. Fortunately, that is often obtained by smoothing.

In addition, you have to specify to optimizers exactly which point to clamp/release. They don’t have to be internal and you can restrict their movement to a curve – for example, if you wanted to move outer points too. But if you’re satisfied with the outer points (such as in the case of this tutorial), there’s a convenience method that adds PlaneClamps for inner points automatically.

optimizer = cb.SketchOptimizer(base)
optimizer.auto_optimize()

However, in this case optimization will make very little difference. It’ll just take a lot of time.

You can read more about mesh optimization in the previous post.

Note: classy_blocks can smooth or optimize a sketch, a specific shape or the whole mesh. More on this in a different post.

classy_blocks example

The full code for this example is available in the GitHub repository. Have fun!