We’ve now completed our overview of neoscore’s built-in functionality! Neoscore offers considerable functionality out of the box, and you can get quite a lot done just using the built-in classes, but for many use-cases its true power is in offering a framework upon which to build your own notation systems.
In general, the best way to learn how to build on top of neoscore is to look through examples. The repository’s examples folder provides many, including a fairly involved example demonstrating how to build a managed notation system by building one for Morton Feldman’s grid notation. We also recommmend looking through the western module’s source code, as in many ways it can be thought of as a whole bunch of examples for things you can build on top of neoscore’s core functionality.
Below we sketch out a few architectural notes that may be helpful when building extensions.
Neoscore’s core functionality is divided into 3 main components: the
core module discussed earlier, a Qt backend for graphics rendering, and an
interface layer translating between the two. Extensions, including
western, mostly integrate with the
core module, but may occasionally need to manipulate interface classes directly. Communication between these architectural layers is generally one directional:
core talks to
interface doesn’t talk back. You should almost never need to deal with the Qt layer directly.
interface layer provides low-level representations of core-layer objects. The document tree at the interface layer is partially flattened; every interface object inside flowable containers is positioned in absolute document coordinates. Interface classes are also immutable; once created they cannot be changed. Mutation in interactive contexts like animation and live-coding is achieved by continually destroying and recreating interface classes.
For those more familiar with GUI systems, neoscore’s interactive runtime acts essentially like an immediate-mode GUI. This is despite the fact that Qt is a retained-mode framework, and that discrepancy is why animations can’t run smoothly. Eventually we would like to migrate to something like imgui to resolve this. There are no technical hurdles to this that we know of, just time constraints. If you want to take a shot at this, get in touch!
When a neoscore document is rendered, an initial
Document.render call is dispatched which fans out
PositionedObject.render calls throughout the document tree depth-first. Each class’s
render implementation is responsible for creating any backing
For objects in flowable containers,
PositionedObject.render delegates to the
PositionedObject.render_in_flowable method. This method looks at the object’s position in the flowable and determines, according to its
breakable_length, which flowable lines it appears on. If the object fits entirely into its first line, it calls
render_complete. Otherwise it calls
render_before_break once at the first line,
render_after_break once at the last line, and
render_spanning_continuation once for each full line in between.
Each of these methods can be overridden by custom classes to create custom rendering behavior.
You can also implement
PositionedObject.post_render_hook to run code immediately before and after document rendering occurs. This is primarily useful for pre-computing expensive properties before rendering. (But be advised that implementations must call their superclass’s hook too.)