Since 2013, Astute Graphics set about developing a state-of-the-art clean sheet application "chassis". Originally intended for use as the basis of a cross-platform vector design application, the final design and implementation allows for a broad range of application types beyond graphical.
Available to license from Astute Graphics [contact us], bypass many years of planning and development and gain access to a very powerful foundation for your software product.
What follows is a technical overview of the Astute Graphics chassis technology. At the time of this writing, the technology notably has a stable and ready-to-use data layer. This page aims at providing a rapid feature overview, mostly concentrated on this aspect of the chassis.
A low-level and domain agnostic functional data structure
Grapes library is the low level layer of our data structure. It implements a nested model following the functional paradigm. Being functional, the main benefits are:
- Immutability: the memory content of an instance is never modified after its construction. This greatly simplifies concurrent and parallel programming, by precluding the condition for data races, without needing to resort to synchronisation.
- Persistence: each modification spawns a modified copy of the data, without altering or deleting the original data. This naturally conserves history, as all different states through time are separate (and co-existing) instances. In addition, random access to the different states is in constant time, as fast as a pointer swap.
This layer is totally orthogonal to the specific domain of the application built on it and can be used to represent vector drawings as well as any other data model (scientific results, spread sheets, video game state, etc.). It relies extensively on node sharing to contain its memory usage. In addition, the memory management is implicit and deterministic: getting rid of root nodes frees all the memory that is not accessible through the remaining roots. The library features a method to get the total memory usage of all allocated nodes in the process.
The ability to nest allows hierarchical data models to be represented naturally. It is made possible by providing a functional implementation of two primitive containers: maps and arrays. As for the STL, those two functional containers are generic and could potentially be reused for general programming.
The Grapes library provides complete serialisation capacity. This is primarily used for saving and loading the data structure to secondary memory (the file system), yet by design it can interact with any C++ stream with random access capacities. This provides a convenient extension point for serialisation to other medium (e.g. network). The data can be serialised in either binary form (for compactness) or textual form (for readability).
Client code relies on a compact API, designed around the central type Grapes::Atom. This type can represent scalar or container values without relying on dynamic polymorphism. All nodes in the data tree are Grapes::Atom. The primary way to manipulate this tree is by static knowledge of its structure (as is usually the case with a data model). Alternatively, a Visitor pattern implementation is available to enable dynamic traversal of the tree.
A secondary and disposable tree, called the red tree, can be built as a facade on top of the primary tree: it allows bottom-to-top traversal of the structure. It has been designed to have very cheap construction and destruction. This red tree enables to edit any node in the tree, as well as a function for consolidated transactions on the data structure: changing several nodes in the tree in a single function call, with better performances than individual changes (because it minimises the number of editions to parent nodes).
Debugging facilities are available. In addition to the textual serialization capacities, a Grapes::drawGraph function is able to produce a DOT representation of a list of nodes and their children, including their relations. (wikipedia.org/wiki/DOT_(graph_description_language))
Ability to specialise for specific domains
The Grapes library can notably be used to build specific data models for any domain. We refer to the realisation of such specialised data model as the DSOs.
Due to the initial intent behind the vector design application, the code base provides a data model to represent a complete vector-drawing document. This functional model, built on top of Grapes, is available in the Clusters library. This data model is open for extension, and can already represent:
- Various artworks (Path, CompoundPath, Rectangle, Ellipse) and Groups
- Styling: Stroke, Fill, Dashes, Brushes, VariableWidth profiles, and the ability to share styles
When a class act as a container for other instances (e,g. a layer containing a collection of artworks), an iterator-based interface is also provided. Those classes come with different helpers, notably one for hit detection on geometry. The library also provides facilities to relocate several DSOs under a new parent, or to replace several DSOs at once.
There are three primary ways to interact with the DSOs nested data structure:
- Statically: the client code relies on explicit knowledge of the defined data model, and in particular the nesting hierarchy. e.g., retrieving the last artwork under the first layer, and getting its transformation.
- Dynamically: the traversal of the nested structure is done by the library code, leveraging a Visitor system. This approach makes it easier to write logic that relies less on its knowledge of the data. e.g., code to count all ellipses in a document, and compute their resulting transformation (by stacking matrices along the different groups leading to said ellipses).
- TransientItems are an extension to the dynamic approach. It extends the Visitation capacities to non-DSO classes (i.e., data types not implemented in term of Grapes), like bezier points in our vector-drawing data model. It also relies on generic programming to optionally attach Payload to the data types, allowing to tailor the Payload content for each data type, while maintaining static safety in data access.
- Tree view (hybrid approach): templated trait classes are provided for each DSO type, giving a static description of its name and structure (number and types of children). Those traits are providing some descriptors available at compile time (~ static introspection). Building on those traits, a Composite pattern allows to uniformly and explicitly traverse the tree structure through virtual function calls.
Serialisation is extended to offer abilities tailored for the document-based applications. In particular, the SingleFile filing class allows for random access to saved document instances (by opposition to sequential loading of history), and to properly share nodes at the file system level across application executions.
Selections are a subtle issue when using a functional data structure (compared to a more usual mutable approach). Let's define the logical identity of an element as a way to identify this element across its state changes: when a given element's state changes, its address changes too. The address cannot be used interchangeably with the identity anymore. The application's chassis provides a system to track logical identity, relying on UIDs, with static (i.e. compile time) guarantee that the UID cannot be duplicated. Those UIDs are scoped per document, and behave properly when different documents are opened in the same process.
A cache system is available for DSOs. Its interface is designed for ease of use with regard to the sequence of changes in the data model (e.g. a document based application, where that document is modified through time). When an updated version of the data is available, the cache discards all entries for nodes that were touched. Obtaining a value is a simple function call with a callable able to produce the requested value: the client code is direct, as it does not need to check for availability in the cache, yet the callable is executed only in the event of a cache miss. This system notably relies on the ability to obtain the difference between two states of the data structure. Also, this difference can be obtained by the client code.
A code generator (Vine)
Implementing the DSO classes on top of Grapes can be turned into a systematic task. By identifying the general rules to implement such classes, a code generator was developed. It is applicable to a majority of application domains. The resulting application (Vine) is taking as input a straightforward XML description of the desired data model, and produces the C++ code for DSO classes. It also takes away the majority of maintenance tasks that would otherwise be done manually when updating the data model (e.g. updating the DSO traits, the visitor base system).
Most notably, the XML allows defining types with single inheritance of any depth, containing data members of any type (as single instance, or multiple instances in a container). The data members can have default values, be constructor parameters. The definition file also makes provision for the user to define custom methods on the generated types, acting as customization points.
External types (not defined within the XML) can also be used as data members, through a forward declaration mechanism.
Clusters data model for vector drawing as described above is generated using Vine, and the features provided by Clusters would be available to data models generated for other problem domains.
GUI Widgets (SAW)
SAW is a library defining a catalogue of widgets, initially designed to be used as annotations in an interactive drawing application:
- Floating windows
- Push Buttons and bi-stable buttons
- Drop down menus
- Vector drawings
The particularity of this library is that it defines all its widgets in terms of the DSO classes from the Clusters library. For this reason, it is a prime candidate to display vector drawn widgets, especially if the widgets are shown in a canvas (e.g. a canvas also used for general artwork rendering). Using SAW is even easier for applications operating with Astute Graphics' Clarity as their renderer.
Document to Clarity
DtC library is a layer of translation between the data model defined by Clusters, and the Clarity renderer. It draws the stylised artwork content of a Clusters document by issuing drawing commands for the Clarity API.
State Machine library (SSM)
SSM is a library allowing to define state machines by relying on metaprogramming. The client code statically defines the list of states, as well as the event for each transition. SSM has an extensive set of features:
- Action (callable) on states Entry/Exit
- Action (callable) on transitions
- Nesting: state machines can be used as state to other state machines
- Nested state machines can also define multiple Input and Output pins, which can be used to define different event paths.
- State machine can contain orthogonal regions (listing a state for each region). When such orthogonal region is entered, all its listed states are entered, and those nested states will evolve in parallel as each following event is received.
The major advantage to this meta programming approach is that the transition accept heterogeneous user defined event types, while maintaining complete type safety: the compiler will catch if any callable (entry/exit/transition action) is defined on an event of incompatible type.