Home Reference Source

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.

Main page

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.

Main page

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);

FrameManager Module

The FrameManager module has two core classes: Screen, and Area.

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.