Introduction
Path.ux is a small app framework inspired by the architecture of Blender, a 3D animation/modeling/visualization app.
Architecture
History
Path.ux is roughly based on Blender's architecture.
The Blender 2.5 project refactored the internal architecture into a rough MVC pattern. The model is the core code, the view is the UI, and the controller is the glue between them.
The controller is called "RNA" and it uses the concept of object paths. So if you have an object, you could look up type information (and the value of) a property with a simple path, e.g. "object.subobject.some_property". Blender uses RNA for its user interface, its python api and its animation system (you can associate object paths with animation curves).
Internally the controller has a special wrapper API for blender's c-struct-based pseudo-objects. This works extremely well as it keeps type information that's only used by the UI out of the model codebase (e.g. the model might not care that a specific color should be clamped to 0-2 instead of 0-1). Even better, the object model presented by the controller need not match the internal data structures.
Since the controller provides type info to the UI a lot of messy boilerplate is avoided, leading to very consise layout code:
def panel(layout, context):
row = layout.row
row.prop(context.object, "some_object_property")
row.tool("mesh.subdivide")
NStructJS
NStructJS is a little library for saving/loading highly structured JS objects as binary data
(for more info, see the official documentation ).
It is not suited for unstructed data (use JSON for that).
NStructJS arose out of the following shortcomings of JSON:
- JSON allocates objects twice.
- JSON is slow compared to what you can get with a structured binary format
The idea of NStructJS is to attach little scripts to your classes that define that class's data and how it is saved. For example:
class SomeClass {
constructor() {
this.data1 = 0;
this.data2 = [1, 2, 3];
this.obj = [some object];
}
//reader "fills in" fields in a newly created object with loaded data
loadSTRUCT(reader) {
reader(this);
super.loadSTRUCT(reader);
}
}
SomeClass.STRUCT = `
my_module.SomeClass {
data1 : int;
data2 : array;
obj : int | this.obj.id;
}
`;
nstructjs.register(SomeClass);
Control How Fields Are Saved
You can use little code snippets to control how fields are saved. For example, if you want to save an integer ID instead of a reference for an object property, you might do this:
my_module.AnotherClass {
someclass : int | this.someclass !== undefined ? this.someclass.id : -1;
}
Versioning
To a certain extend nstructjs will gracefully handle version changes. The basic idea is to save a copy of your struct scripts with each file, that way each file knows how to load itself.
Tool System
Tools are what the user uses to change state in the model. They handle undo, can take control of events if needed and in general are foundational to path.ux and it's associated projects.
Tools all inherit from ToolOp, which roughly looks like this (see Context section for an explanation for what the "ctx" parameters are):
class SomeTool extends ToolOp {
static tooldef() {return {
uiname : "Tool",
toolpath : "module.tool"
inputs : ToolOp.inherit({
}), //use ToolOp.inherit to flag inheritance
outputs : {}
}}
static invoke(ctx, args) {
/*create a new tool instance.
args is simple key:val mapping
where val is either a string, a number
or a boolean.*/
//super.invoke will create tool and parse args
return super.invoke(ctx, args);
}
//add a 2d line
makeDrawLine(v1, v2, css_color);
//reset temporary drawing geometry
resetTempGeom();
calcUndoMem(ctx) {
return size in bytes of stored undo data
}
undoPre(ctx) {
//create undo data
}
undo(ctx) {
//execute undo with data made in previous call to this.undoPre
}
exec(ctx) {
//execute tool
}
modalStart(ctx) {
super.modalStart(ctx);
//start interactice mode
}
modalEnd(was_cancelled) {
super.modalEnd(was_cancelled);
//end interactive mode
}
on_[mousedown/mousemove/mouseup/keydown](ctx) {
//interactive mode event handler
}
ToolOp.register(SomeTool);
}
Context
The foundation of the tool system is a special Context struct that's provided by client code. Think of it as defining "arguments" for tools. Path.ux can use any context struct, but requires the following properties be defined:
class Context {
get api() {
//return reference to a controller.ModelInterface
}
get appstate() {
//return reference to main appstate global
}
get screen() {
//return reference to main FrameManager.screen
}
}
In addition, path.ux has hooks to provide UI context, specifically which are is currently active. To do this, either override the following methods in ScreenArea.Area.prototype, or subclass Area:
//called when area should be considered "active"
push_ctx_active() {
}
//called when area should be considered "inactive"
pop_ctx_active() {
}
Undo
Typically tools will inherit from a base class with a general, brute-force undo (i.e. saving the entire application and then reloading it on undo). Additionally to save on speed and memory subclasses can override undoPre and undo with their own implementation.
tooldef()
Tools have a special tooldef() static function that "defines" the tool. It returns things like what properties the tool has, it's name, it's path in the data path system, etc.
Tool Properties
Tools have input and output slots. See toolprop.js. There are integer properties, float properties, various linear algebra properties (vectors, matrices), enumerations, bitflags, and in addition client code may provide it's own property classes.
Tool Properties
Tool Properties are generic typed value containers. They store things like:
- Numbers (floats, integers)
- Vectors
- Simple lists
- Enumerations
- Bitflags
Tool properties also store various UX related data:
- Ranges
- Tooltips
- UI names
- Slider step sizes
- Number units
Tool properties are used by the ToolOp API (the basic operators that implement the undo stack) as well as the datapath controller API (where they internally provide type information for the application model)
API
The basic interface for tool properties is:
class ToolProperty { getValue() { } setValue(v) { } //add event callback, type should be 'change' on(type, cb) { } }
Context design:
Context is a simplified API to access the application model. Contexts are passed around to ToolOps and used by path.ux.
Required fields
Call contexts are required to have the following fields:
- screen -- the active FrameManager.Screen (or a subclass of it)
- api -- the data path controller, (controller.ModelInterface)
- toolstack -- The tool operator stack (simple_toolsys.ToolStack)
Context Overlay:
A context overlay is a class that overrides context fields. It has a validate() method that is polled regularly; if it returns false the overlay is removed.
Contexts can be "frozen" with .lock. When frozen they should have no direct object references at all, other then .state, .datalib and .api.
Properties can control this with "_save" and "_load" methods inside of the overlay classes, as well as overriding saveProperty and loadProperty inside of Context subclasses.
Example of a context overlay:
class ContextOverlay {
validate() {
//check if this overlay is still valid or needs to be removed
}
static contextDefine() {return {
flag : 0 //see ContextFlags
//an example of inheritance. inheritance is automatic for fields
//that are missing from contextDefine().
flag : Context.inherit([a bitmask to or with parent])
}}
get selectedObjects() {
/*
if you want to get a property from below the stack in the ctx
use this.ctx or return Context.super
*/
if (some_reason) {
//tell ctx to
return Context.super();
} else {
//this will also work
return this.ctx.selectedObjects;
}
return this.ctx.scene.objects.selected.editable;
}
selectedObjects_save() {
let ret = [];
for (let ob of this.selectedObjects) {
ret.push(ob.id);
}
return ret;
}
selectedObjects_load(ctx, data) {
let ret = [];
for (let id of data) {
ret.push([lookup object from data using (possible new) context ctx])
}
return ret;
}
}
Locked contexts
Locked contexts are contexts whose properties are "saved", but not as direct references. Instead, each property is (ideally) saved as some sort of ID or datapath to look up the true value on property access.
We suggest you subclass Context and implement saveProperty and loadProperty methods.
class Overlay extends ContextOverlay {
get something() {
return something;
}
something_save() {
return this.state.something.id;
}
something_load(ctx, id) {
return [lookup id somewhere to get something];
}
}
Tool Contexts
We encourage you to put Context properties related to the view inside a separate ContextOverlay. That way you can keep ToolOps from accessing the view by feeding them a special context that lacks that overlay (but note that tools in modal mode should always get a full context).
class ToolOverlay extends ContextOverlay {
static contextDefine() {return {
name : "tool"
}}
get mesh() {
return this.state.mesh;
}
get material() {
return this.state.material;
}
}
Context.register(ToolOverlay);
class ViewOverlay extends ContextOverlay {
static contextDefine() {return {
name : "view",
flag : ContextFlags.IS_VIEW
}}
get screen() {
return this.state.screen;
}
get textEditor() {
return
}
}
Context.register(ToolOverlay);
History
Path.ux is roughly based on Blender's architecture.
The Blender 2.5 project refactored the internal architecture into a rough MVC pattern. The model is the core code, the view is the UI, and the controller is the glue between them.
The controller is called "RNA" and it uses the concept of object paths. So if you have an object, you could look up type information (and the value of) a property with a simple path, e.g. "object.subobject.some_property". Blender uses RNA for its user interface, its python api and its animation system (you can associate object paths with animation curves).
Internally the controller has a special wrapper API for blender's c-struct-based pseudo-objects. This works extremely well as it keeps type information that's only used by the UI out of the model codebase (e.g. the model might not care that a specific color should be clamped to 0-2 instead of 0-1). Even better, the object model presented by the controller need not match the internal data structures.
Since the controller provides type info to the UI a lot of messy boilerplate is avoided, leading to very consise layout code:
def panel(layout, context):
row = layout.row
row.prop(context.object, "some_object_property")
row.tool("mesh.subdivide")
Context design:
Context is a simplified API to access the application model. Contexts are passed around to ToolOps and used by path.ux.
Required fields
Call contexts are required to have the following fields:
- screen -- the active FrameManager.Screen (or a subclass of it)
- api -- the data path controller, (controller.ModelInterface)
- toolstack -- The tool operator stack (simple_toolsys.ToolStack)
Context Overlay:
A context overlay is a class that overrides context fields. It has a validate() method that is polled regularly; if it returns false the overlay is removed.
Contexts can be "frozen" with .lock. When frozen they should have no direct object references at all, other then .state, .datalib and .api.
Properties can control this with "_save" and "_load" methods inside of the overlay classes, as well as overriding saveProperty and loadProperty inside of Context subclasses.
Example of a context overlay:
class ContextOverlay {
validate() {
//check if this overlay is still valid or needs to be removed
}
static contextDefine() {return {
flag : 0 //see ContextFlags
//an example of inheritance. inheritance is automatic for fields
//that are missing from contextDefine().
flag : Context.inherit([a bitmask to or with parent])
}}
get selectedObjects() {
/*
if you want to get a property from below the stack in the ctx
use this.ctx or return Context.super
*/
if (some_reason) {
//tell ctx to
return Context.super();
} else {
//this will also work
return this.ctx.selectedObjects;
}
return this.ctx.scene.objects.selected.editable;
}
selectedObjects_save() {
let ret = [];
for (let ob of this.selectedObjects) {
ret.push(ob.id);
}
return ret;
}
selectedObjects_load(ctx, data) {
let ret = [];
for (let id of data) {
ret.push([lookup object from data using (possible new) context ctx])
}
return ret;
}
}
Locked contexts
Locked contexts are contexts whose properties are "saved", but not as direct references. Instead, each property is (ideally) saved as some sort of ID or datapath to look up the true value on property access.
We suggest you subclass Context and implement saveProperty and loadProperty methods.
class Overlay extends ContextOverlay {
get something() {
return something;
}
something_save() {
return this.state.something.id;
}
something_load(ctx, id) {
return [lookup id somewhere to get something];
}
}
Tool Contexts
We encourage you to put Context properties related to the view inside a separate ContextOverlay. That way you can keep ToolOps from accessing the view by feeding them a special context that lacks that overlay (but note that tools in modal mode should always get a full context).
class ToolOverlay extends ContextOverlay {
static contextDefine() {return {
name : "tool"
}}
get mesh() {
return this.state.mesh;
}
get material() {
return this.state.material;
}
}
Context.register(ToolOverlay);
class ViewOverlay extends ContextOverlay {
static contextDefine() {return {
name : "view",
flag : ContextFlags.IS_VIEW
}}
get screen() {
return this.state.screen;
}
get textEditor() {
return
}
}
Context.register(ToolOverlay);
NStructJS
NStructJS is a little library for saving/loading highly structured JS objects as binary data
(for more info, see the official documentation ).
It is not suited for unstructed data (use JSON for that).
NStructJS arose out of the following shortcomings of JSON:
- JSON allocates objects twice.
- JSON is slow compared to what you can get with a structured binary format
The idea of NStructJS is to attach little scripts to your classes that define that class's data and how it is saved. For example:
class SomeClass {
constructor() {
this.data1 = 0;
this.data2 = [1, 2, 3];
this.obj = [some object];
}
//reader "fills in" fields in a newly created object with loaded data
loadSTRUCT(reader) {
reader(this);
super.loadSTRUCT(reader);
}
}
SomeClass.STRUCT = `
my_module.SomeClass {
data1 : int;
data2 : array;
obj : int | this.obj.id;
}
`;
nstructjs.register(SomeClass);
Control How Fields Are Saved
You can use little code snippets to control how fields are saved. For example, if you want to save an integer ID instead of a reference for an object property, you might do this:
my_module.AnotherClass {
someclass : int | this.someclass !== undefined ? this.someclass.id : -1;
}
Versioning
To a certain extend nstructjs will gracefully handle version changes. The basic idea is to save a copy of your struct scripts with each file, that way each file knows how to load itself.
Tool Properties
Tool Properties are generic typed value containers. They store things like:
- Numbers (floats, integers)
- Vectors
- Simple lists
- Enumerations
- Bitflags
Tool properties also store various UX related data:
- Ranges
- Tooltips
- UI names
- Slider step sizes
- Number units
Tool properties are used by the ToolOp API (the basic operators that implement the undo stack) as well as the datapath controller API (where they internally provide type information for the application model)
API
The basic interface for tool properties is:
class ToolProperty { getValue() { } setValue(v) { } //add event callback, type should be 'change' on(type, cb) { } }
Tool System
Tools are what the user uses to change state in the model. They handle undo, can take control of events if needed and in general are foundational to path.ux and it's associated projects.
Tools all inherit from ToolOp, which roughly looks like this (see Context section for an explanation for what the "ctx" parameters are):
class SomeTool extends ToolOp {
static tooldef() {return {
uiname : "Tool",
toolpath : "module.tool"
inputs : ToolOp.inherit({
}), //use ToolOp.inherit to flag inheritance
outputs : {}
}}
static invoke(ctx, args) {
/*create a new tool instance.
args is simple key:val mapping
where val is either a string, a number
or a boolean.*/
//super.invoke will create tool and parse args
return super.invoke(ctx, args);
}
//add a 2d line
makeDrawLine(v1, v2, css_color);
//reset temporary drawing geometry
resetTempGeom();
calcUndoMem(ctx) {
return size in bytes of stored undo data
}
undoPre(ctx) {
//create undo data
}
undo(ctx) {
//execute undo with data made in previous call to this.undoPre
}
exec(ctx) {
//execute tool
}
modalStart(ctx) {
super.modalStart(ctx);
//start interactice mode
}
modalEnd(was_cancelled) {
super.modalEnd(was_cancelled);
//end interactive mode
}
on_[mousedown/mousemove/mouseup/keydown](ctx) {
//interactive mode event handler
}
ToolOp.register(SomeTool);
}
Context
The foundation of the tool system is a special Context struct that's provided by client code. Think of it as defining "arguments" for tools. Path.ux can use any context struct, but requires the following properties be defined:
class Context {
get api() {
//return reference to a controller.ModelInterface
}
get appstate() {
//return reference to main appstate global
}
get screen() {
//return reference to main FrameManager.screen
}
}
In addition, path.ux has hooks to provide UI context, specifically which are is currently active. To do this, either override the following methods in ScreenArea.Area.prototype, or subclass Area:
//called when area should be considered "active"
push_ctx_active() {
}
//called when area should be considered "inactive"
pop_ctx_active() {
}
Undo
Typically tools will inherit from a base class with a general, brute-force undo (i.e. saving the entire application and then reloading it on undo). Additionally to save on speed and memory subclasses can override undoPre and undo with their own implementation.
tooldef()
Tools have a special tooldef() static function that "defines" the tool. It returns things like what properties the tool has, it's name, it's path in the data path system, etc.
Tool Properties
Tools have input and output slots. See toolprop.js. There are integer properties, float properties, various linear algebra properties (vectors, matrices), enumerations, bitflags, and in addition client code may provide it's own property classes.