A lot of early, hand drawn PCBs had this look, where every connection is smooth and slightly melted.
When people first switched to using computers to design circuit boards, the routing options were very limited and traces were forced to have sharp corners. Some people forget that our tendency to restrict traces to 45° angles is mainly a holdover from before there were any other options.
Some of the benefits of teardrops and smooth traces are controversial, but the following can generally be agreed upon:
That last point is the one people tend to dwell on. It's been shown that at sub-GHz frequencies, the difference is negligible. Many developers argue that since there are no EMC benefits, this feature is not needed. The attitude of "only ignorant people who know nothing about EMC would want rounded traces" really irks me.
There are two important points to make here.
A lot of people have attempted to add these missing features into the KiCad source code, but for various reasons the most promising branches never got merged. Curved traces are apparently on the roadmap for KiCad 6, which hopefully isn't too far in the future, but I'm not holding my breath.
Another way to add functionality is via "action plugins" – python scripts. This is in many ways more limited, but has the massive benefit of not needing to recompile the KiCad source code.
Interactive curved traces are not possible this way, but I had an idea: to draw a PCB with sharp corners, and then "melt" it afterwards, via a plugin. Curves can be approximated with many small straight segments, and by keeping the original file you can still easily modify it, then apply the melting process as a step that renders to a new file.
I found another repo which repackages the earlier script as an action plugin, although it doesn't run on Windows builds. Due to a horrible chain of dependencies and problems, Windows KiCad builds are statically bundled with Python 2 and the older wxWidgets API. The problem is trivial and only relates to the GUI dialog box.
However, both versions of the script have a subtle problem which may not be immediately apparent.
The algorithm progressively chamfers corners, splitting traces on each iteration if their angle is more than a threshold. The split position is based on either a maximum length parameter, or a percentage of the length of the current trace, whichever is smaller. But the algorithm fails to account for the change in length immediately caused by the chamfer in progress – as a result, the rounding is never symmetrical.
This might not matter for some designs, but is catastrophic when a PCB has a bus of traces all spaced at the minimum clearance, which is very common. The traces are processed in essentially random order, so the uneven curves cause a flood of DRC errors.
Notice that two of the traces have been processed in the opposite direction, and the uneven curves mess up the clearances. While we could just space the traces out further apart, there is no reason the script shouldn't be able to apply an even rounding, so the clearances remain consistent.
It's worth bearing in mind that the subdivisions are applied evenly, there are no parameters except for the number of iterations. The fact that the action plugin asks for two extra enigmatic parameters ("length %" and "scaling %") already raises some warning flags. We do want to limit the maximum radius of the curves, but that should be a single parameter.
Digging into the source code, "length %" is the shortening amount as a function of the shortest trace at an intersection. It took me a while to get my head around this, but the default value of 50% means that twice as many passes are needed.
To go through the process in detail, consider a single L-shaped trace. If the traces are infinitely long, the first subdivision has to decide where to truncate, and this is where our radius parameter can act.
The next chamfer needs to cut the new corners, so the logical place to connect is roughly a quarter of the way along the current chamfer.
The third pass cuts these corners, and so on. Notice that for a smooth curve, every segment is of the same size.
In the rounding script, setting the "length %" to a quarter should make the segments almost, but not quite equal lengths. It still doesn't account for the strange results we were seeing. In fact, the core problem with the script is that after chamfering one corner, the length of the segment has changed, and the wrong value is used for the adjacent corner.
Storing the original segment lengths at each pass in a dictionary is sufficient to fix the problem. At last, the curves are predictably smoothed, and we no longer have a million DRC errors on our bus!
I wasn't quite content to leave it there, however. To ensure a perfect rounding, we need to add a cosine factor. I worked out on paper some triangles and came up with a new formula for the segment length percentage. Plugging this into the script finally lets us pass the most demanding test: a square. If the rounding works correctly, it should turn into a perfect circle.
Using even segments ensures that the curve is as smooth as can be for a given number of passes. I found three passes to generally be plenty, there's nothing stopping you from running more but if the segments become smaller than the feature size then it's a bit pointless. Kicad does start to slow down when there are thousands of segments, and DRC errors appear repeatedly for a single violation. However, the updated subdivision routine asymptotically approaches a fixed curve, and is guaranteed to keep at least one segment of each input track. Two passes is enough to spot and fix the majority of DRC problems, then the final board can be made with more.
Marvellous. Now onto the harder part...
However, only a handful of the teardrops were generated correctly for me. The algorithm assumes every track approaches the pad or via directly, never off-axis, which is only true for a small amount of them. Again, we could go through the PCB and manually touch these up, but that really isn't what I had in mind.
A very unsatisfactory teardrop. Ideally, every pad should look like it was dipped in honey, a minimum-energy solution, continuously differentiable. That means the polygon edges must be tangent where they connect to the pad and to the track.
To do this, I upgraded the plugin to use cubic Bezier curves. A cubic curve has four points: a start, an end, and two control points. If we ensure that the control point where the curve connects to the via is tangential, and the control point where the curve connects to the track is tangential, then the curve will always join to them smoothly. The remaining question is how much "weight" to give each control point. The teardrops are controlled by two parameters, essentially a length and a width, but even keeping those dimensions fixed, changing the weight can make a teardrop change from fat to lean.
I found it helpful to draw some curves in Photoshop just to play around.
After fiddling with various options, I ended up making the weights functions of the dimension parameters. Deciphering the action plugin took some brain power, but eventually I was generating some very pleasing shapes.
If the teardrop width is less than 100%, the mating point needs to be rotated slightly. The screenshot below isn't exactly a disaster, but it would be unsatisfying to leave this unfixed.
To get this to work properly, I had to rewrite how the length of the teardrop is calculated, finding the intersection of the track with the perimeter of the via. There is probably an elegant mathematical way to determine it, but since the distance is very short I took the simple brute-force approach.
The leftmost teardrop shows a very short trace approaching at an angle. It took a lot of experimentation to come up with Bezier weights that looked good in all situations. Importantly, when a track approaches off-centre, the weighting on one side must be different than the other.
I foresaw two courses of action here. One, we could edit the rounding script to hold off segementing traces that are too close to a pad. Two, we could edit the teardrop plugin to read this situation, follow the track along its curve, and start the teardrop from further back. While the first option is probably easier, a common situation is a small 45° bend right before a via, and if that does not get rounded, the teardrop will need to be a very strange shape. So I opted to walk along the track if we can.
I should probably stop and complain about the python scripting process for these action plugins. As mentioned, it's Python 2 on windows and Python 3 on linux. The plugins get cached, so any changes mean re-launching KiCad. Infuriatingly, most error messages are dumped to /dev/null but if you do capture them, they'll probably be wrong as the info relates to the uncached python files. It feels like there's a solution to this problem (simply deleting the .pyc files didn't work) but I couldn't figure it out.
There is also a python scripting console, which has its own problems, but at least if the script is importable, it can be
reload()'d without having to restart the application. The teardrop plugin has been specifically written so that it can be invoked from either the scripting console or as an action plugin, which is very nice, compared to the track rounding plugin.
But my main complaint is that things are poorly documented. There's a small amount of doxygen reference material but it doesn't directly correspond with the exposed python API. The best place to look for help seems to be other people's action plugins. Miles McCoo, who is credited in the source of several plugins, has a blog which offers a lot of helpful information.
With that said, the actual algorithm to walk back along the connected track is not as scary as it might seem.
If the connected track is shorter than the target length, then we search for traces with same net, same layer, and the same start or end point. Each one is consumed until we have reached the desired length, or can't find another track. The end position is used as a vector to start the teardrop.
As shown in the shot above, this doesn't give a perfect result for arbitrary, unrealistic angles, but when used with sensible traces, or rounded ones, the results are exactly as good as I'd hoped.
Playing with the parameters gives different results, but all of them are satisfyingly gloopy. If you want really fat teardrops, they're now available!
An edge case is where following the trace has multiple paths. In the case of a T junction, rounded into a smooth Y, following just one track causes an odd teardrop shape to form. Since there isn't really a good way to fix this, when the code encounters a junction in the trace it stops there and places a smaller teardrop. This generates the least-bad looking result.
Of course, placing the via on the track will create a double-teardrop. You can break the track to ensure this happens.
The main feature missing here is to make the curves automatically DRC compliant. By fixing the subdivision logic, dense traces no longer throw stupid errors, but there are still problems that have to be corrected if the source PCB is laid out incorrectly. The ideal solution would be to detect DRC errors as they happen, the way the "highlight collisions" routing mode does, and choose an appropriate bending radius accordingly. This does imply either a directed approach (doing the innermost traces first) or a more complex iterated approach, since, in the case of the bus of identical tracks, only the innermost one can be curved without initially causing a DRC error. To be able to curve slightly, then come back and curve some more when space is available, is going to be a very difficult process with the subdivision method.
It seems the python API is the limiting factor here, since none of the DRC system is accessible. But, I think the algorithm would go something like this:
The script is perfectly useable as-is. I have a few more ideas I want to add, it would be nice to be able to select a group of tracks and subdivide only them. A very useful feature would be to limit the minimum angle between tracks. This would set a termination point, where doing more passes has no effect. The benefit would be that working on the processed output file becomes viable – if you want to edit a track, delete the curve, draw a new one, and re-run the script, and only those new tracks will be affected.
Another possible improvement: I said that using zones for teardrops never causes DRC errors, which is not strictly true. If two teardrops overlap, that will cause a problem. The updates to allow off-centre teardrops means this is more likely, at least if the teardrops are big. An easy way to fix this is to increment or randomize their zone priority, so one always takes precedence. This would remove (or make more difficult) the "remove all teardrops" option since the zone priority is used as an ID, but that isn't an issue when you're rendering to a new file with the track rounding.
The track-rounding script has a bit of an odd history, it seems each contributor has produced a new repo leaving the git history behind. It's a mess, but since I also plan to make more modifications to it, I figured I'd continue the tradition and make a fresh repo myself. I attempted to merge together the git histories of the other two repos, to try and clear things up, although it's still missing some of the beginning. The plugin can be found here.