Refactoring from single to multi purpose
published at 16.09.2020 09:38 by Jens Weller
Save to Instapaper Pocket
For the second time this year I'm refactoring a program from a single purpose to have two or more modes of operation. Both times the start and end result is similar, just the processing is different. A classic case of using polymorphism.
The first program was a tool to process videos from speakers, find a face and export the subframe around it into a new video. The first mode was a good approach to test it, and gave results for some speakers. The second mode did a complete scan first, and then gave a smoother video for some videos. Still, both modes had to be retained.
With the current program its similar. Its was written in 2017, to create the Meeting C++ t-shirt. Its never was meant to do anything else, and is from its implementation straight forward. Now its 2020, and I liked the "Meeting C++17" shirt from back then, that I'm thinking about creating a "Meeting C++20" one. Lots of the code can be reused, as exporting to SVG via QPainter or the first step of scanning a mask image is still the same. Just the steps to create a design now needs to be able to run different versions. Also I'm refactoring the code from C++14 to C++17, but I will cover this in a second post.
The code is available on github, if you want to play around with it. But be warned, the code for the C++17 shirt can allocate a lot of memory if it goes to deep into details.
The old code
Before I go into the glory of how C++17 transforms the old C++14 code, lets see what did I write 3 years ago?
Short overview on classes, note that all terms and names are my own, stuff I though would be fitting 3 years ago.
- Qt UI classes
- Mainwindow - the class the is the main window and contains menu handling code
- StepWidget - a UI interface that allows the visual code to be processed step by step, also has the code to export to SVG.
- Processing classes
- PixelGroupRunner - a QRunnable derived class to scan a mask image and form groups of pixels that are connected.
- PixelGroup - a class that collects a group of pixels and represents different operations like isInGroup(Point)
- PixelModel - this class holds a PixelGroup instance while the image processing happens in PixelGroupRunner.
- Penrose pattern classes
- PenroseShape - repesents a single pixel group that in the processing step now gets filled with penrose tiles
- PenroseShapeGroup - this class is the interface to process the different PenroseShapes
- PenroseTiling - this class contains the code to calculate the penrose tiling
- Triangle - a helper struct representing a single triangle in the penrose tiling and its Color.
And that should already be all the classes that are in active use to create a design based on the penrose tiling. There are a few others, which mostly are left from the prototyping and some new ones to create a different design for C++20.
Penrose Tiling code
Most of this code will not be touched in the refactoring, as its working code that runs the old mode. The refactoring will add the ability to have more then one hard coded mode. The penrose tiling is calculated with complex numbers, and std::complex provides then with real and imag the x and y coordinates for the painter.
The code for the penrose tiling it self is explained quite well in this video from 2017:
For the t-shirt there needed to be a change though, once a triangle of the penrose tiling has all of its 3 points in the same pixel group, it will not be processed into smaller tiles:
void PenroseShape::step() { /*auto is_not_in_refrect = [this](const Triangle& t){ return group.isInRefRect(t); }; penrosetiling.filter(is_not_in_refrect); auto is_in_group = [this](int x, int y){return group.isInGroup(Point(x,y));}; auto triangle_in_group = [&is_in_group](const Triangle& t) { return is_in_group(t.a.real(),t.a.imag()) && is_in_group(t.b.real(),t.b.imag()) && is_in_group(t.c.real(),t.c.imag()); };*/ auto find_fitting_triangle = [this](const Triangle& t) { auto vec = divide(t); for(const auto& t : vec) { if(!triangle_in_group(t)) return false; } std::vector v; for(const auto& t : vec) { divide(v,t); divide(v,t,TilingType::DARTS); } for(const auto& t : v) { if(!triangle_in_group(t)) return false; } placed.push_back(t); return true; }; penrosetiling.filter(find_fitting_triangle); penrosetiling.levelup(); }
Originally I had a few more lambdas in the code before I refactored these into members, for this post I left them in the code as its more compact and clear to see what happens. The filter method of penrose tiling simply calls remove_if and then erase to remove the triangles from the next processing step, which happens in levelup.
Join the Meeting C++ patreon community!
This and other posts on Meeting C++ are enabled by my supporters on patreon!