Marco Cobeña Morián
Software Development Engineer
Disclaimer: PointNet has been recently renamed Typhoon; however, some of the following images still display the former, as were taken before the renaming.
Typhoon is a tool developed by Plain Concepts for viewing, measuring and editing point clouds, Gaussian Splattings and BIM models, made with Evergine. After iterating it during the last years, we faced a need: how to allow third parties creating new features on top of Typhoon. Instead of having our single team working on the codebase, how could we allow others in an asynchronous and parallel fashion to extend its core?
In this article we show the new architecture based on plug-ins and, how such has helped, in a real scenario, to simplify the work of different teams over a common code base.
One of its multiple features is the automatic generation of BIM files from point clouds, also known as Digital Twins. These twins empower companies by surfacing information in their buildings in a digital manner: imagine an industrial building which shows digital alerts if real components reach a limit temperature, for example. It supports the most common file types: E57, LAS/Z and PCD. It also works with PLY and SPLAT for Gaussian Splatting and IFC, the de facto standard in CAD environments.
–
Before all that, we were one year working with a codebase written in Python which, compiled into Windows EXE files, were called directly from Typhoon, in a CLI fashion. We had no better approach to mix .NET and Python worlds by that time.
Bundling Python into executables has pros and cons. One of the worst cons was its size: we used to work with EXE files of ~2.5 GB. When you double click on it, it takes ~1 min to “uncompress” the entire Python environment and then, start executing our code. It was a pain to deal with in a daily basis. (Some time after we solved this issue thanks to the integration of CSnakes.)
One approach we hardly considered is moving Python to the cloud, having a lightweight Web API in the middle. However, given the nature of point cloud processing, to delegate computation in the cloud is not cheap when using machines with powerful GPUs; and, occasionally, when you have an enough powerful GPU in your machine, it may be much more interesting executing such processes locally.
Finally, we came up with plug-ins: they have the benefit of still stressing our local GPUs, but even this can be moved to the cloud and still be hidden for the final user. Where did we start from?
[JF1]Esta frase no está muy bien expresada, se entiende la idea pero le daría una vuelta. Por ejemplo “delegar la computación en cloud no es barato al utilizar máquinas con GPUs muy potentes y en ocasiones cuando cuentas con un GPU suficientemente potente en local puede ser mucho más interesante poder ejecutar dichos procedimientos en local”
When we defined the new architecture diagram, we thought of an entry point shared by every plug-in: a simple interface which self-explains what a plug-in can do (and cannot). We named it IPlugIn and distributed it in its own package: Typhoon.PlugIns. In the end, it is a lightweight DLL .NET assembly which is the only dependency a plug-in has against Typhoon.
Nowadays, IPlugIn allows:
For example, a plug-in cannot add a new panel at the right side; instead, it is forced to populate its own window which Typhoon controls. We have tried, in build time, to enforce some of the requirements a plug-in has. How, then, Typhoon actually detects plug-ins?
Typhoon is already distributed internally with a bunch of plug-ins, all of them focused on decreasing the number of points in a point cloud (with down-sampling techniques, for instance).
Every plug-in lives under Plug-ins folder, right at the same level as Typhoon.Windows.exe (Typhoon’s entry point). Inside, each plug-in has its own subfolder, where all of its dependencies are stored: see .NET ones, or assets (images, JSONs, etc.), for example.
What happens if a plug-in depends on a package reference already present in Typhoon, but with a different version? We cannot load the same assembly with two different versions at the same time, at least by default. In the early days of .NET, this was accomplished through App Domains; nowadays, such has been replaced with Assembly Load Context. It is a built-in mechanism to load an assembly (DLL here) from disk and decide whether its dependencies are loaded from the companion files or from the host. This way, as we will see in the next section, plug-ins can call ImGui (the technology used to make the entire User Interface in Typhoon) and, such is shared between Typhoon and them. We pay the bill of potentially having the exact same assemblies repeated on disk (i.e. two plug-ins share dependencies); however, we consider this is not a problem in 2025 given the small sizes and cost of hardware.
Plug-ins can also require live instances of dependencies through Dependency Injection. If a plug-in needs to work with the current point cloud, it can by simply adding IIOService to its public constructor, and Typhoon does the rest. If a plug-in wants to alert the user in any moment, it can do the same with IAlertService. Furthermore, this flats the path to unit testing, which is a common practice in our team, by leveraging a simple and decoupled architecture.
Thanks to our built-in integration with ImGui (and to our own binding as well: ImGui.Net), the entire UI has been built using just this. It has the benefits of rapid prototyping, plus the styling which allows us to give it a fresh look.
During the different iterations, we have played with different approaches to split ImGui code into different components which communicate between them. Thanks to Evergine’s component-based model, both have fit perfectly, helping us to achieve a more maintainable software. When we decided how each plug-in could define its own UI, it was natural to stick with ImGui. We had already experience with decoupled UI components, and we imagined plug-ins in the same way.
Typhoon is in charge of asking every plug-in which menu item they want to show under Plug-ins (at top menu bar). Once the user clicks on one of them, Typhoon directly calls IPlugIn.DrawWindow() and, what happens inside, is responsibility of the plug-in.
Some plug-ins may have a list of inputs and a button, others a tab view with a more complex set-up. Developers have the entire ImGui’s catalog at their hands. This insanely simple approach allows to convey plug-ins’ UX from Typhoon. For example, every plug-in has a window with its name in the tittle bar, and it can be closed with the typical ‘x’ button at the right side. This way, teams can asynchronously take their own decisions on their plug-ins while all of them convey to an homogeneous solution.
As you can appreciate, plug-ins started having a certain complexity and we needed an approach to build on solid.
As soon as we put in practice plug-ins, bugs started to appear here and there. One of them was as simple as crashing Typhoon because a plug-in had a ‘.’ (dot) in its name (it is a common pattern to name things with dots in .NET world). How could we reproduce it without having to manually create a new plug-in? Without manually debugging Typhoon until the crash? Avoiding regressions from now on?
Thanks to Evergine.Mocks, we thought, we could wake plug-ins up in unit tests, and it worked! In order to reproduce the example from bellow, we simply added a new minimal plug-in (default implementation of IPlugIn, remember) and named it with a dot in the middle.
It is easy to imagine this opened us a world of testing for before mentioned dependencies, status report, etc.
As soon as the requirements started to grow, we took the decision of opening a door for more richer interactions.
One particular requirement we got at the beginning was highlighting an area in the point cloud. This means a plug-in can interact with the current scene in some way. “Highlighting an area” was translated into drawing a wireframed cube through Evergine’s LineBatch3D and, this inside a Drawable3D component.
Our first approach was to enrich IPlugIn with the ability to inject a component which Typhoon would take care of. We would add such into an entity we control, thus the plug-in could just play with its component (disabling or enabling it, for instance). Although we liked it, we could fall down into having IPlugIn full of small needs, or use cases, so we moved into a broader option.
Why not we allow plug-ins to inject entities? An entity can be as simple as having one component, and as complex as one could imagine. In the future, one plug-in may add a new point cloud format, for example. On the other side, an entity has the potential power to traverse the other entities and modify their shapes (i.e. removing components, for instance), or accessing the main camera, etc. However, we decided to trust, and adjust our implementation based on the results.
Thanks to this approach teams can leverage their knowledge on Evergine and its component-based architecture, making plug-ins a transparent glue between Typhoon and them.
The idea of extending Typhoon through plug-ins have ended up being a practical solution. We have been working simultaneously up to three different teams (even with external companies) creating multiple plug-ins. The synchronization between all of them was significantly decreased thanks to sharing a common contract with clear specifications: the IPlugIn interface, how plug-ins are installed and show up in the UI and, how they can interact with the scene through their own entities.
Nowadays, most of the plug-ins are developed in house; however, we would like others to create new ones as well, as soon as we make Typhoon publicly available.
In the meantime, we continue evolving, and have in mind features as the following:
We are thrilled imaging what you may build on top of Typhoon, and which challenges we may encounter in the road.
Marco Cobeña Morián
Software Development Engineer
Cookie | Duration | Description |
---|---|---|
__cfduid | 1 year | The cookie is used by cdn services like CloudFare to identify individual clients behind a shared IP address and apply security settings on a per-client basis. It does not correspond to any user ID in the web application and does not store any personally identifiable information. |
__cfduid | 29 days 23 hours 59 minutes | The cookie is used by cdn services like CloudFare to identify individual clients behind a shared IP address and apply security settings on a per-client basis. It does not correspond to any user ID in the web application and does not store any personally identifiable information. |
__cfduid | 1 year | The cookie is used by cdn services like CloudFare to identify individual clients behind a shared IP address and apply security settings on a per-client basis. It does not correspond to any user ID in the web application and does not store any personally identifiable information. |
__cfduid | 29 days 23 hours 59 minutes | The cookie is used by cdn services like CloudFare to identify individual clients behind a shared IP address and apply security settings on a per-client basis. It does not correspond to any user ID in the web application and does not store any personally identifiable information. |
_ga | 1 year | This cookie is installed by Google Analytics. The cookie is used to calculate visitor, session, campaign data and keep track of site usage for the site's analytics report. The cookies store information anonymously and assign a randomly generated number to identify unique visitors. |
_ga | 1 year | This cookie is installed by Google Analytics. The cookie is used to calculate visitor, session, campaign data and keep track of site usage for the site's analytics report. The cookies store information anonymously and assign a randomly generated number to identify unique visitors. |
_ga | 1 year | This cookie is installed by Google Analytics. The cookie is used to calculate visitor, session, campaign data and keep track of site usage for the site's analytics report. The cookies store information anonymously and assign a randomly generated number to identify unique visitors. |
_ga | 1 year | This cookie is installed by Google Analytics. The cookie is used to calculate visitor, session, campaign data and keep track of site usage for the site's analytics report. The cookies store information anonymously and assign a randomly generated number to identify unique visitors. |
_gat_UA-326213-2 | 1 year | No description |
_gat_UA-326213-2 | 1 year | No description |
_gat_UA-326213-2 | 1 year | No description |
_gat_UA-326213-2 | 1 year | No description |
_gid | 1 year | This cookie is installed by Google Analytics. The cookie is used to store information of how visitors use a website and helps in creating an analytics report of how the wbsite is doing. The data collected including the number visitors, the source where they have come from, and the pages viisted in an anonymous form. |
_gid | 1 year | This cookie is installed by Google Analytics. The cookie is used to store information of how visitors use a website and helps in creating an analytics report of how the wbsite is doing. The data collected including the number visitors, the source where they have come from, and the pages viisted in an anonymous form. |
_gid | 1 year | This cookie is installed by Google Analytics. The cookie is used to store information of how visitors use a website and helps in creating an analytics report of how the wbsite is doing. The data collected including the number visitors, the source where they have come from, and the pages viisted in an anonymous form. |
_gid | 1 year | This cookie is installed by Google Analytics. The cookie is used to store information of how visitors use a website and helps in creating an analytics report of how the wbsite is doing. The data collected including the number visitors, the source where they have come from, and the pages viisted in an anonymous form. |
attributionCookie | session | No description |
cookielawinfo-checkbox-analytics | 1 year | Set by the GDPR Cookie Consent plugin, this cookie is used to record the user consent for the cookies in the "Analytics" category . |
cookielawinfo-checkbox-necessary | 1 year | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary". |
cookielawinfo-checkbox-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary". |
cookielawinfo-checkbox-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary". |
cookielawinfo-checkbox-necessary | 1 year | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary". |
cookielawinfo-checkbox-non-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Non Necessary". |
cookielawinfo-checkbox-non-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Non Necessary". |
cookielawinfo-checkbox-non-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Non Necessary". |
cookielawinfo-checkbox-non-necessary | 1 year | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Non Necessary". |
cookielawinfo-checkbox-performance | 1 year | Set by the GDPR Cookie Consent plugin, this cookie is used to store the user consent for cookies in the category "Performance". |
cppro-ft | 1 year | No description |
cppro-ft | 7 years 1 months 12 days 23 hours 59 minutes | No description |
cppro-ft | 7 years 1 months 12 days 23 hours 59 minutes | No description |
cppro-ft | 1 year | No description |
cppro-ft-style | 1 year | No description |
cppro-ft-style | 1 year | No description |
cppro-ft-style | session | No description |
cppro-ft-style | session | No description |
cppro-ft-style-temp | 23 hours 59 minutes | No description |
cppro-ft-style-temp | 23 hours 59 minutes | No description |
cppro-ft-style-temp | 23 hours 59 minutes | No description |
cppro-ft-style-temp | 1 year | No description |
i18n | 10 years | No description available. |
IE-jwt | 62 years 6 months 9 days 9 hours | No description |
IE-LANG_CODE | 62 years 6 months 9 days 9 hours | No description |
IE-set_country | 62 years 6 months 9 days 9 hours | No description |
JSESSIONID | session | The JSESSIONID cookie is used by New Relic to store a session identifier so that New Relic can monitor session counts for an application. |
viewed_cookie_policy | 11 months | The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data. |
viewed_cookie_policy | 1 year | The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data. |
viewed_cookie_policy | 1 year | The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data. |
viewed_cookie_policy | 11 months | The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data. |
VISITOR_INFO1_LIVE | 5 months 27 days | A cookie set by YouTube to measure bandwidth that determines whether the user gets the new or old player interface. |
wmc | 9 years 11 months 30 days 11 hours 59 minutes | No description |
Cookie | Duration | Description |
---|---|---|
__cf_bm | 30 minutes | This cookie, set by Cloudflare, is used to support Cloudflare Bot Management. |
sp_landing | 1 day | The sp_landing is set by Spotify to implement audio content from Spotify on the website and also registers information on user interaction related to the audio content. |
sp_t | 1 year | The sp_t cookie is set by Spotify to implement audio content from Spotify on the website and also registers information on user interaction related to the audio content. |
Cookie | Duration | Description |
---|---|---|
_hjAbsoluteSessionInProgress | 1 year | No description |
_hjAbsoluteSessionInProgress | 1 year | No description |
_hjAbsoluteSessionInProgress | 1 year | No description |
_hjAbsoluteSessionInProgress | 1 year | No description |
_hjFirstSeen | 29 minutes | No description |
_hjFirstSeen | 29 minutes | No description |
_hjFirstSeen | 29 minutes | No description |
_hjFirstSeen | 1 year | No description |
_hjid | 11 months 29 days 23 hours 59 minutes | This cookie is set by Hotjar. This cookie is set when the customer first lands on a page with the Hotjar script. It is used to persist the random user ID, unique to that site on the browser. This ensures that behavior in subsequent visits to the same site will be attributed to the same user ID. |
_hjid | 11 months 29 days 23 hours 59 minutes | This cookie is set by Hotjar. This cookie is set when the customer first lands on a page with the Hotjar script. It is used to persist the random user ID, unique to that site on the browser. This ensures that behavior in subsequent visits to the same site will be attributed to the same user ID. |
_hjid | 1 year | This cookie is set by Hotjar. This cookie is set when the customer first lands on a page with the Hotjar script. It is used to persist the random user ID, unique to that site on the browser. This ensures that behavior in subsequent visits to the same site will be attributed to the same user ID. |
_hjid | 1 year | This cookie is set by Hotjar. This cookie is set when the customer first lands on a page with the Hotjar script. It is used to persist the random user ID, unique to that site on the browser. This ensures that behavior in subsequent visits to the same site will be attributed to the same user ID. |
_hjIncludedInPageviewSample | 1 year | No description |
_hjIncludedInPageviewSample | 1 year | No description |
_hjIncludedInPageviewSample | 1 year | No description |
_hjIncludedInPageviewSample | 1 year | No description |
_hjSession_1776154 | session | No description |
_hjSessionUser_1776154 | session | No description |
_hjTLDTest | 1 year | No description |
_hjTLDTest | 1 year | No description |
_hjTLDTest | session | No description |
_hjTLDTest | session | No description |
_lfa_test_cookie_stored | past | No description |
Cookie | Duration | Description |
---|---|---|
loglevel | never | No description available. |
prism_90878714 | 1 month | No description |
redirectFacebook | 2 minutes | No description |
YSC | session | YSC cookie is set by Youtube and is used to track the views of embedded videos on Youtube pages. |
yt-remote-connected-devices | never | YouTube sets this cookie to store the video preferences of the user using embedded YouTube video. |
yt-remote-device-id | never | YouTube sets this cookie to store the video preferences of the user using embedded YouTube video. |
yt.innertube::nextId | never | This cookie, set by YouTube, registers a unique ID to store data on what videos from YouTube the user has seen. |
yt.innertube::requests | never | This cookie, set by YouTube, registers a unique ID to store data on what videos from YouTube the user has seen. |