Here's you can find some documentations and guidelines to contribute to augmented_carpentry.
All commits need to be labeled with a tag among these:
git commit -m "ADD:<description>" <--- for adding new elements
git commit -m "FIX:<description>" <--- for fixing (errors, typos)
git commit -m "FLASH:<description>" <--- quick checkpoint before refactoring
git commit -m "MILESTONE:<description>" <--- for capping moment in development
git commit -m "CAP:<description>" <--- for for less important milestones
git commit -m "UPDATE:<description>" <--- for moddification to the same file
git commit -m "MISC:<description>" <--- for any other reasons to be described
git commit -m "WIP:<description>" <--- for not finished work
git commit -m "REFACTOR:<description>" <--- for refactored code
git commit -m "MERGE:<description>" <--- for merging operations
You can merge few tags e.g.:
git commit -m "WIP-CAP:<description> <--- for cap moment in not finished work
DiffCheck is distributed as a Python Grasshopperr plug-in via yak and its source code via PyPI. The plug-in is composed by a series of .ghuser
components.
There are 3 ways you can contribute to the Python GH plug-in:
- By adding new components to the plug-in.
- By fixing bugs of existing components in the plug-in.
- By adding new functionalities to existing components in the plug-in.
Before committing to the repository you need to have tested the components in the Grasshopper environment and be sure that this is working correctly. Also, provide a sufficient documentation in the PR (for now) please.
Follow these steps to develop and test the Python GH plug-in:
Download this repo if you haven't already.
Next, if you used ACPy (AugmentedCarpentryPython) before as an end-user clean all the ACPy folders
in the following directory (the last name will change):
C:\Users\<user-name>\.rhinocode\py39-rh8\site-envs\default-wMh5LZL3
note that if you drop an official released ACPy component from yak, this one will have the
#r : ACPy==<version_number>
notation at the top of the script. Get rid of all these release components before to start and be sur to erase again the previous folders (they recreated each time#r : ACPy
is called).
Build the package from the py source code's directory:
python setup.py sdist bdist_wheel
Lastly, install the pip pacakge from the repository in editable mode. This way, all the modifications made to the source code of the repository will be reflected in the installed package. Open a terminal and run the following command (replace the path with where you download the repository):
C:\Users\<your-username>\.rhinocode\py39-rh8\python.exe -m pip install -e "<path-to-repository-root>\src\gh\ACPy"
For your info the packages is installed in C:\Users\andre\.rhinocode\py39-rh8\Lib\site-packages
.
That's it you are now a contributor to the ACPy! We raccomand to not download anymore from yak package but rather use the source code in the repository. If you want the latest ACPy, checkout and pull the main.
For DiffCheck there are 2 main folders in the repository:
src/gh/ACPy/components
here you can add new components or modify existing ones (for more info on how to create one we point you to this documentation). Here we call thesrc/gh/ACPy/ACPy
this is our package where the core functionalities are implemented.
The idea is to start by developing the content of the component in the file src/gh/ACPy/diffCgeck_app.py
. This would be a simple script that contains the logic of the component. Once the script ACPy_app.py
is working correctly, you can move the code to the component file in the src/gh/ACPy/components
folder. This is because the component file is the one that will be componentized and distributed via yak.
We reccomand to use VSCode
as IDE for developing the components. This is because it has a good integration with the Grasshopper
environment and it is easy to debug the components. To set up the IDE follow these steps:
- Install the
ScriptSync
extension forVSCode
. - Install the
ScriptSync
from the yak manager in Rhino. - Open the
ACPyApp.py
from thesrc/gh/ACPy/components
folder you are working on inVSCode
, and set its path to the ScriptSync ghcomponent. - If you modify the code in
VSCode
, the changes will be reflected in the Grasshopper component as soon as you save inVSCode
again thecode.py
. - Once your code is working, prepare the code and componentize it.
If you want to use the GHEditor it's ok but everytime you modify the pakcage or the component's code, after any modifications you need to restart the Python interpreter from the ScriptEditor (Tools > Reload Python3 (CPython) Engine
) and recompute the solution in Grasshopper.
Prepare your component as explained here. You can componentize it locally and test it in Grasshopper. Here's how to componentize:
python f:\ACPy\src\gh\util\componentizer_cpy.py --ghio "C:\Users\andre\.nuget\packages\grasshopper\8.2.23346.13001\lib\net48\" .\src\gh\components\ .\build\gh
Note that you need to find the path to your GHIO folder. This is the folder where the
Grasshopper.dll
is located. E.g. You can find it in thenuget
folder in the Rhino installation directory.
Once you are sure that the component is working correctly, you can push the changes to the repository.
The release will be made via CI from main. As a contributor you don't need to worry about this. The plug-in is componentized, pushed to yak/PyPI and the user can download the latest version from yak.
More to come.
Here's the naming convention for this project:
localVariable
: lowerCamelCase.type PrivateVariable
: public member of a classtype m_PrivateVariable
: Hungarian notation with UpperCamelCase for private class members.static type s_StaticVariable
: Hungarian notation with UpperCamelCase for static members of class.APP_SPEC
: Constants with SNAKE_UPPER_CASE.- All the other naming uses UpperCamelCase.
Here's an example:
// do not use using namespace std; we specify the namespace everytime
std::foo()
// next line graph style
void Foo()
{
/* content */
}
// structure name uses UpperCamelCase
struct AnExampleStruct
{
// structure attribute uses UpperCamelCase
const char* Name;
};
// class name uses UpperCamelCase
class AnExampleClass
{
public:
AnExampleClass(const int& init);
virtual ~AnExampleClass();
// member functions use UpperCamelCase
void PublicMemberFunction()
{
// local variable uses lowerCamelCase
int localVariable = 0;
}
// A field indicator to separate the functions and attributes
public:
int PublicVariable;
// Private member function block
private:
// member functions use UpperCamelCase
void PrivateMemberFunction();
// Also a field indicator to separate the functions and attributes
private:
// private variables uses Hungarian notation with UpperCamelCase
int m_PrivateVariable; // m_VariableName for normal variable
static int s_Instance; // s_VariableName for static variable
};
// Start headers with
#pragma once
// Start declarations with precompiled headers
#include "aiacpch.h"
For documentation we use the *JavaDoc" convention. Follow this guide for documenting the code.
/**
* @brief fill a vector of TSPlanes from a yaml file containing their corners data
* @param filename path to the map.yaml file
* @param planes vector of TSPlane objects
*/
We use an .ini file to store the config parameters of the application. The parser is derived from this project but with some modification. Here's an example:
// Just include this header
#include "AIAC/Config.h"
// Open file "config.ini".
// The second param indicate if the file need to be update when
// InsertEntry() and UpdateEntry() is called.
// The initialization happens in the very beginning of `ACApp.cpp`.
inih::Ini config("config.ini", true);
In order to get rid of plain text, where an typo can cause unexpected error, we strongly suggest you to declare a static const string variable in config.h
and call it in your code. For each data entry, "section" and "name" are combined as the primary key. Here's an example:
In Config.h
, declare the variable for key:
inline static const std::string SEC_TEST = "SectionTest";
inline static const std::string VALUE_NAME_1 = "ValueName1";
inline static const std::string VALUE_NAME_2 = "ValueName2";
Usage:
// Get<type>(section, key, default value if not exist)
AIAC::Config::Get<int>(AIAC::Config::SEC_AIAC, AIAC::Config::VALUE_NAME_1, 10);
// InsertEntry(section, key, value)
AIAC::Config::InsertEntry(AIAC::Config::SEC_AIAC, AIAC::Config::VALUE_NAME_2, -1);
// Insert a vector
vector<int> primeVector = {2, 3, 5, 7, 11};
AIAC::Config::InsertEntry("SectionTestVector", "PrimeNumber", primeVector);
// Get<T>(section, key, default_value)
// The entry will be created if not exist.
cout << AIAC::Config::Get<int>("SectionTest", "ValueName1", -1) << endl;
cout << AIAC::Config::Get<string>("SectionTest", "ValueName2", "I'm a string!") << endl;
// With vector, use GetVector() instead of Get(),
for(auto n: AIAC::Config::GetVector<int>("SectionTestVector", "PrimeNumber", primeVector)){
cout << n << " ";
}
cout << endl;
// Update an Entry
AIAC::Config::UpdateEntry("SectionTest", "key1", 999);
cout << AIAC::Config::Get("SectionTest", "key1", -1) << endl;
// For update an vector entry, call the same function as normal
primeVector.push_back(13);
AIAC::Config::UpdateEntry("SectionTestVector", "PrimeNumber", primeVector);
// Write the ini to the original file
AIAC::Config::WriteToFile();
// Write to another file
AIAC::Config::WriteToFile("another_config_file.ini");
After running thie code above, you gets the following output:
-1
I'm a string!
2 3 5 7 11
999
with two identical .ini
file contains:
[SectionTest]
key1 = 999
key2 = I'm a string!
[SectionTestVector]
PrimeNumber = 2 3 5 7 11 13
AC uses a precompile header aiacpch.h
to the project to shorten compilation time for headers that you rarely modify such as stdb library, opencv etc.. Add to aiacpch.h
every big header you do not use often.
Include at the very top #include "aiacpch.h"
of every .cpp
file.
Layers are the main component of the framework. Each layer gets stacked and executed in that order. Each layer represents a different unit e.g. TSLAM, camera access, 3Drender, etc. Each layer has events where code can be injected in the loop and custom events.
namespace AIAC
{
class Layer
{
public:
virtual ~Layer() = default;
/// Is called when the layer is attached to the application.
virtual void OnAttach() {}
/// Is calle before GLFW poll events, GL frame and Imgui Frame
virtual void OnFrameAwake() {}
/// Is called when frame starts
virtual void OnFrameStart() {}
/// Is called when frame ends
virtual void OnFrameEnd() {}
/// Is called when the GLFW, GL and Imgui frame is updated and rendered
virtual void OnFrameFall() {}
/// Is called when the layer is detached from the application (~app).
virtual void OnDetach() {}
};
}
Next, you need to add it to the stack call in the main file CApp where it fits most:
...
acApp_ptr->GetWindow()->Init();
acApp_ptr->GetGORegistry()->Init();
acApp_ptr->PushLayer<AIAC::LayerCamera>();
acApp_ptr->PushLayer<AIAC::LayerCameraCalib>();
acApp_ptr->PushLayer<AIAC::LayerSlam>();
acApp_ptr->PushLayer<AIAC::NewLayer1>(); // <---------- e.g.: your new layer 1
acApp_ptr->PushLayer<AIAC::LayerModel>();
acApp_ptr->PushLayer<AIAC::LayerToolhead>();
acApp_ptr->PushLayer<AIAC::LayerFeedback>();
acApp_ptr->PushLayer<AIAC::LayerUI>();
acApp_ptr->PushLayer<AIAC::NewLayer2>(); // <---------- e.g.: your new layer 2
acApp_ptr->GetRenderer()->Init();
acApp_ptr->GetEventBus()->Init();
acApp_ptr->Run();
There is one only app and it can be accessed from layers with:
AIAC::Application& app = AIAC::Application::GetInstance();
or easier with a macro
AIAC_APP
To get other layers' variables use a small query function from the Application class. In the case of accessing test_a of layerA from e.g. layerB:
AIAC_APP.GetLayer<AIAC::LayerA>()->test_a
UI is mainly built in ImGui. We wrap some of the functions and add custom functionalities.
Logos addresses/data is store in CustomLogos.h
. As an example:
#define AIAC_LOGO_LIGHT_GRAY "assets/images/logos/logo_linux_gray_light.png"
E.g. to consume it:
m_LogoBlack = AIAC::Image(AIAC_LOGO_BLACK);
Colors macros can be found/add in the header ClrPalette.h
with the following definition style:
#define AIAC_UI_DARK_GREY ImVec4(0.2f, 0.2f, 0.2f, 1.0f)
For every new layer you can create a new collapsable pannel in the main UI. To do so follow the next steps.
Create a new function (declare it in LayerUI.h > Class LayerUI
and implement it in LayerUI.cpp
) and start calling ImGui
methods from there, put everythin you need for the UI there. This is the only place where you will write UI for your pane, like so:
void LayerUI::SetPaneUICamera()
{
ImGui::Text("This layer is responsible for the physical camera.");
AIAC::Camera& camera = AIAC_APP.GetLayer<AIAC::LayerCamera>()->MainCamera;
ImGui::Text("Camera is %s", camera.IsOpened() ? "open" : "closed");
ImGui::Text("Camera resolution: %d x %d", camera.GetWidth(), camera.GetHeight());
}
Next copy past the template function in LayerUI.cpp
and reference the function you created:
// Label Collapse PaneContent
StackPane(PaneUI("Example", true, AIAC_BIND_EVENT_FN(SetPaneUIExample) ));
StackPane(PaneUI("Camera", true, AIAC_BIND_EVENT_FN(SetPaneUICamera) ));
StackPane(PaneUI("Slam", true, AIAC_BIND_EVENT_FN(SetPaneUISlam) ));
StackPane(PaneUI("<your-new-name>", true, AIAC_BIND_EVENT_FN(YourNewContainerMethod) ));
The file dialog widget we use is implemented as singleton from this repo. This means that only one file dialog widget can be opened at once (it is possible to implement it as multiple instance if needed). If you need to select a file from local system, here's the snippet:
if (ImGui::Button("Open 3dModel"))
ImGuiFileDialog::Instance()->OpenDialog("Choose3dModel", "Open 3dModel", ".ply, .obj", ".");
if (ImGuiFileDialog::Instance()->Display("Choose3dModel"))
{
if (ImGuiFileDialog::Instance()->IsOk())
{
std::string filePathName = ImGuiFileDialog::Instance()->GetFilePathName();
std::string filePath = ImGuiFileDialog::Instance()->GetCurrentPath();
// action
/* write here what to do with the file path */
}
ImGuiFileDialog::Instance()->Close();
}
We implement a bus-like event system based on the observer pattern and the tppevent repo. It is not multi-threaded but it can be modified as such. All the event files are contained in the dir AIAC/EventSys
.
The event bus is stored in the Application. You can raise events from other files in two ways, either synchronusly, the event will be fired immediately with:
AIAC_EBUS->DispatchEvent(std::make_shared<SmtHappenedEvent>(param))
Or a-synchronusly, the event will be stored in the bus queue and executed in the main loop, before any layer frame calls:
IAC_EBUS->EnqueueEvent(std::make_shared<CameraCalibrationLoadedEvent>(filePathName));
Note that you need to pass a
std::shared_ptr<Event>
to the event raisers.
First Add the event type in Event.h
if it does not exist already:
enum class EventType
{
None = 0,
AppClose,
SLAMMapLoaded, SLAMVocabularyLoaded,
CameraCalibrationLoaded,
ExampleCalled // <-- example
/* add types of events here */
};
And a category in the same file:
enum EventCategory
{
None = 0,
EventCategoryApplication = BIT(0),
EventCategorySLAM = BIT(1),
EventCategoryCamera = BIT(2),
EventCategoryExample = BIT(3) // <-- example
/* add event category here */
};
Next, create a new event file with a class that inherits from class Event
, as an example ExampleEvent.h
. Remeber to add to the parent constructor (a) EventType, (b) EventCategory and (c) (accessory, by default 0) the prority at which the event needs to be executed, the bigger the number the sooner in the queue it will be fired:
namespace AIAC
{
class ExampleCalledEvent : public Event
{
public:
explicit ExampleCalledEvent(const std::string param)
: Event(EventType::ExampleCalled, EventCategory::EventCategoryExample, 100), m_Param(param)
{}
void OnExampleCalled();
private:
std::string m_Param;
};
}
Do not forget to add the header to AIAC.h
in the correct order.
#include "AIAC/EventSys/Event.h"
#include "AIAC/EventSys/ExampleEvent.h"
/* >> add types of events here << */
#include "AIAC/EventSys/EventBus.h"
In the declaration you can access layers via AIAC_APP
as usual:
namespace AIAC
{
void ExampleCalledEvent::OnExampleCalled()
{
AIAC_APP.GetLayer<LayerName>()->Func(m_Param);
}
}
Finally you just need to add a listener to the Init()
function of EventBus.h
:
#include "AIAC/EventSys/ExampleEvent.h"
m_EventQueue.appendListener(EventType::ExampleCalled, [](const EventPointer& event) {
auto& slamEvent = static_cast<ExampleCalledEvent&>(*event);
slamEvent.OnExampleCalled();
});
To log use the following MACROS. All the code is contained in Log.hpp
and Log.cpp
.
AIAC_INFO("test_core_info");
AIAC_WARN("test_core_warn");
AIAC_CRITICAL("test_core_critical");
AIAC_DEBUG("test_core_debug");
AIAC_ERROR("test_core_error");
The output is like so:
[source main.cpp] [function main] [line 32] [16:30:05] APP: test
The logging can be silenced by setting OFF the option in the main CMakeLists.txt
and do clean reconfiguration.
option(SILENT_LOGGING "Do not log messages in the terminal of on." ON)
To add, remove or get Geometric Objects (GO) from any layer:
#include "AIAC/GOSys/GO.h"
std::shared_ptr<GOPoint> pt1 = GOPoint::Add(1, 1, 1); // create a point
std::shared_ptr<GOPoint> pt1copy = GOPoint::Get(pt1.GetId()); // get the point elsewhere
float x = pt1copy->X(); // access GO properties
auto pts = GOPoint::GetAll(); // access all GO of one type
GOPoint::Remove(pt1copy.GetId()); // remove the point from registry
Note the the Constructor for each GO is private, the only way to instance it is to use the Add()
static function. The reason why we decided to adopt this "registry" pattern is because all GO object need to be created as a smart pointer to be copied into a GOregistry. We must be sure that every GOObject is subscribed to the GORegistry. A normal c++ constructor cannot create the smart pointer of the create object, hence the Add() function does this for us. All the next layers will retrive those smart pointers from the GOregistry and eventually modify them. And finally the render will go through the GOregistry and "bake" (render) all the GO geometries.
The Geometric Object System allows to add objects to the 3D scene before to render them. Having a geometric entity prior to the OpenGL Render allows to calculates relations between geometries to obtain live feedbacks (e.g. drilling depth). Files can be found in /GOSys
.
The GOSystem is composed of two components:
GOPrimitives.h/cpp
(a library which storesGOPoint
,GOLine
,GOText
, etc)GORegistry.h/cpp
(an API which interacts with the map. Note that it has to recive only smart pointers to avoid object slicing since we pass heirs ofGOPrimitive
.)
The renderer API is separated into two parts:
GlUtils.h
: Provides a direct interface to draw the 3 basic material of OpenGL, i.e.Point
,Line
, andTriangle (mesh)
. By calling the function, it creates an object, render it and delete instantly. These functions should only be used when GO system doesn't fit the scenrio.RenderAPI.h
: To render the already exist GO Primitives. When a GO is initialized or update, it creates an OpenGL object and stores in the memory. The functions in this file takes the GO object as the parameter and renders the corresponding OpenGL objects.
GlUtils.h
contains the function to draw the 3 basic material of OpenGL, i.e. point, line, and triangle (mesh). By calling the function, it creates an OpenGL object, render it, and delete instantly. Since calling these functions continuesly add/delete OpenGL objects, which is not efficient, they should only be used when GO system doesn't fit the scenrio. (e.g. drawing SLAM tags while mapping, since it changes every frame)
void glDrawPoints3d(const std::vector<glm::vec3> &vertices, const std::vector<glm::vec4> &colors, GLfloat pointSize);
void glDrawPoints3d(const std::vector<glm::vec3> &vertices, const glm::vec4 &color, GLfloat pointSize);
vertices
: A vector of 3d points, indicate the position of the vertices.colors
: A RGBA(0~1.0) color, can be either a singleglm::vec4
or a vector with the same size of thevertices
.pointSize
: The size of the point.
void glDrawLines3d(const std::vector<glm::vec3> &edges, const std::vector<glm::vec4> &colors);
void glDrawLines3d(const std::vector<glm::vec3> &edges, const glm::vec4 &color);
/*
* (0, 1, 0) --- (1, 0, 0)
* | |
* | |
* | |
* (0, 0, 0) --- (0, 0, 1)
*
* If you want to draw a square like this, you should construct the `edges` as:
* [
* (0, 0, 0), (0, 0, 1),
* (0, 0, 1), (1, 0, 0),
* (1, 0, 0), (0, 1, 0),
* (0, 1, 0), (0, 0, 0)
* ]
*
* */
edges
: A vector of the edge's end-points.colors
: A RGBA(0~1.0) color, can be either a singleglm::vec4
or a vector with the same size of theedges
.
void glDrawTriangles3d(const std::vector<glm::vec3> &vertices, const std::vector<uint32_t> &indices, const std::vector<glm::vec4> &colors);
void glDrawTriangles3d(const std::vector<glm::vec3> &vertices, const std::vector<uint32_t> &indices, const glm::vec4 &colors);
/*
* P2(0, 1, 0) --- P3(1, 0, 0)
* | / |
* | / |
* | / |
* | / |
* | / |
* P0(0, 0, 0) --- P1(0, 0, 1)
*
* If you want to draw a mesh of square like this, you should construct the `edges` as:
* [
* (0, 0, 0), // P0
* (0, 0, 1), // P1
* (1, 0, 0), // P2
* (0, 1, 0), // P3
* ]
* with `indices`:
* [
* 0, 1, 3, // Right-bottom triangle
* 3, 2, 0 // Left-top triangle
* ]
* */
vertices
: A vector of 3d points, indicate the position of the vertices.indices
: A vector of all triangle's indices.colors
: A RGBA(0~1.0) color, can be either a singleglm::vec4
or a vector with the same size of the vertices.
Render API is a collection of functions that are used for rendering 3D objects (GO primitives and the map).
These functions can be call directly as long as RenderAPI.h
is included.
An implicit type casting is implemented, simply call the DrawGO
function in the Render.cpp
's function OnRender()
:
void DrawGO(const shared_ptr<GOPrimitive>& goPrimitive);
void DrawGOs(const std::vector<shared_ptr<GOPrimitive>>& goPrimitive);
Rendering text through OpenGL is quite tricky. The TextRenderer.h
is here to handle this.
The TextRenderer
has to be initialized, after that, static functions can be call for rendering texts.
void Renderer::Init() {
...
TextRenderer::Init()
...
}
void myFunction() {
// First setting the current projection matrix
TextRenderer::SetProjection(projection); // The MVP projection of the scene
// Rendering text at (0, 3, 0)
TextRenderer::RenderTextIn3DSpace(
"center", // Text to show
glm::vec3(0.0f, 3.0f, 0.0f), // Position in 3D space
glm::vec4(0.0f, 0.0f, 0.0f, 0.7f)); // Color
// Rendering the text on a fixed position on the screen.
// (0, 0) is the left-bottom corner and (windowWidth, windowHeight) is the right-top corner
TextRenderer::RenderText(
"center", // Text to show
150.0f, 150.0f, // Position on the screen
glm::vec4(0.0f, 0.0f, 0.0f, 0.7f)); // Color
}
void DrawSlamMap(const shared_ptr<tslam::Map> &map, const glm::vec4 &color, float pointSize);
map
: The map itself.color
: The color of the point clouds.pointSize
: The point cloud's size.
other shapes
When necessary, c++ testing is done by using CTest. Important/critical features (e.g., correcting functioning of graphics with OpenGL and Glfw) needs testing to be written (this is usefull for e.g., GitHub Actions). Such tests can be extracted from the main source code and integrated in a seperate section: cmake testing.
To add a new test do as follow.
First create a new sub-folder in the folder ./test
as ./test/exampletest
.
Here add a console cpp file called tester.cpp
which returns 0 or 1 and add a new CMakeLists.txt
as such:
add_executable(example_test tester.cpp)
/* <--
Insert here linking necessary for the executable
Note that if you already found packages in the head CMakeLists file
you can simply use the macros here.
--> */
add_test(NAME "ExampleTest" COMMAND "example_test" <argv-here> WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}")
In the ./test
's CMakeLists.txt
add the created sub-directory:
if (TEST_EXAMPLE)
add_subdirectory(exampletest)
endif()
Finally add an option in the main CMakeLists.txt
describing the test:
include(CTest)
# ...
option(TEST_EXAMPLE "Test to test something important." ON)
# ...
if(TEST_EXAMPLE)
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/tests/exampletest)
endif()
Next, ./configure.sh -c
and ./build.sh
and:
cd ./build
ctest -N # <--- to see how many tests there are
ctest -V # <--- run the tests