Coding Guide

ProtoPedia users can (1) build compound items such as diagrams with code, but can also (2) code their own visual elements and kits, and place them in their catalog directories where they become available in ProtoPedia's UI, and can be shared with other users. In addition, the core technology is openly available under the MIT Licence for use in any application (3). This document provides the information needed for all three activities.

Sections 5 - 7 describe how to work with prototype trees, the material from which all ProtoPedia elements (referred to as "items") are made. The remaining sections concern construction of and interaction with 2d shapes implemented by way of SVG.

1. Sample code

Before wading into the details, you might like to look at some code. Samples 2,3,4 illustrate how new visual elements are built.

  1. A simple diagram.
  2. An element: rectangle
  3. A kit: ring

The following link provides access to the code which implements ProtoPedia's visual elements:

https://protopedia.org/catalog.html

Select the item of interest in the catalog, and then click the "Open Code" button.

2. Working with code

2.1 The Code Editor

The editor enables you to edit code in one window, re-run it at will, and see the results in the graphics panel immediately. If you click on the name of the source file, or on one of the dependencies, an ordinary text editor is popped on the file in question.

If you have signed in, the save as button will be active. Each signed in user is allocated a file system, where files are named according to the scheme

(username)/whatever.js

This is where the code editor saves files. You can open the main drawing program (also called the "structure editor") on whatever via :

https://protopedia.org/draw.html?source=(username)/whatever.js

2.2 Debuggers

Browser debuggers (eg Chrome's DevTools) work well with ProtoPedia. However, the usual variants at https://protopedia.org/code.html and https://protopedia.org/draw.html catch errors, which is not what is wanted in the context of debugging. The variants https://protopedia.org/coded.html and https://protopedia.org/drawd.html (accepting exactly the same GET arguments) are provided in which errors are not caught.

3. Require

Components are accessed and defined via core.require. Here is an example:

 core.require('/shape/circle.js','/arrow/arcArrow.js',
  function (circlePP,arrowPP) {
    let item = svg.Element.mk('<g/>');
    ...
    return item;
  }
)

This binds the variables circlePP and arrowPP to the components defined in '/shape/circle.js', and '/shape/arcArrow.js', respectively, and then calls the function (ie the last argument to core.require). The value returned by the function is the newly built component.

4. Catalogs

Catalogs are the visual lists of items from which elements are dragged in the structure editor during the insert and swap operations. They are the principal means by which visual elements implemented in code are made available in the structure editor, and shared among the users of ProtoPedia.

You can create your own catalog by saving visual elements at locations of the form of the form /catalog/<whatever>. Then, after such a save, the next time you start up the structure editor, you will see your new element under the tab <whatever>, where it can be used in insertions and swaps.

5. Trees

Now to the nitty-gritty: the structure of ProtoPedia's data, and the operations which provide access to it.

All prototype trees (aka "items") are trees: each non-atomic child has a unique parent. (Formally, if parent.prop === child, and prop is an own property of parent, there is no parent2 with parent2 !== parent, and with parent2.prop2 === child for own property prop2 of parent2). The internal nodes in items inherit from prototypes core.ArrayNode for sequential, zero-based arrays, and core.ObjectNode for objects which are not arrays.

  
core.ObjectNode.mk();

creates a new Object, and

core.ArrayNode.mk();

a new Array.

object.set('name',child);

assigns child as a child of object with name name. If child is an Object or Array, this results in setting special properties: child.__parent === object, and child.__name === 'name'.

object.add(child);
 

also assigns child as a child of object, but automatically assigns its name.

For an Array,

array.push(child);

pushes child onto the end of array, and assigns array as the parent of child. That is, array[array.length-1] === child, and if child is an Object or Array, and child.__parent === array. Also, child.__name === array.length-1. Arrays are always sequential and zero-based.


core.lift(obj);

takes an "ordinary" JavaScript tree such as one expressable in JSON, and turns it into the ProtoPedia kind of tree. For example:

core.lift({a:2,b:["a",4]});

will produce a Object/Array tree with matching structure.

Click here for an example of tree construction and manipulation.

X

var root = core.ObjectNode.mk();
var aa = core.ObjectNode.mk();
var bb = core.ObjectNode.mk();

root.set("a",aa); // adds aa as a child named "a" of root
root.set("b",bb);

// __name and __parent "glue the tree together".
bb.__name;
==>b
bb.__parent === root;
==>true

// let's add some atomic data 
aa.x = 5;  // set is not needed for atomic data or functions
aa.f = function (x) {return x*x;}

// now for an Array
var cc = core.ArrayNode.mk();
bb.set("c",cc);

cc.push(core.ObjectNode.mk().set("z",45));
cc.push(5);


this yields:

          --root--
        /          \
       a            b
      / \            \
     x   f            c
    /     \            \
   5    function      [ *, 5]
                       /
                      z
                     /
                    45
   


where * is an Object

This could also be built with

 
var root = core.lift({a:{x:5},b:{c:[{z:45},5]}})
root.a.f = function (x) {return x*x};

There is nothing wrong with having properties of Objects that reference nodes in the tree other than children, as in

a.xx = b; 

in the above example.Then xx is a cross-tree reference.

Restriction on names: names may include only letters, numerals, and the underbar, and may not start with a numeral.

6. Instantiation and Serialization

 
inode = node.instantiate();

builds an instantiation of node; a tree which has the same structure as node, but inherits primitive data and functions from node via prototypical inheritance. (Details here)

Serialization is performed by:

 
s = core.stringify(node);

as described here.

7. No news

Nodes are constructed with core.ObjectNode.mk() and core.ArrayNode.mk(), rather than via the use of a "new". The definition of the mk method for Object is:

core.ObjectNode.mk = function () {
  return Object.create(core.ObjectNode);
}

Recall that Object.create(X) creates a new object with X as prototype.

Object is introduced simply with:

core.ObjectNode = {};

and Array with:

core.ArrayNode = [];

core.ObjectNode itself serves as the prototype for instances. In the more conventional pattern, core.ObjectNode would be a function, and the prototype property of that function, not Object itself, would be the prototype for the instances generated via the new operator (note that if F = function (){}, new F() is equivalent to Object.create(F.prototype))

The function, function.prototype, new   pattern has been available in JavaScript all along, whereas the more direct Object.create was introduced more recent years (in version 1.8.5 to be exact). ProtoPedia employs the newer pattern, not so much for its intrinsic simplicity (though that's nice), but because this way of doing things has a major simplifying effect on the entire ProtoPedia code base.

You will never see a "new" anywhere in the code. The convention is that for prototype X, X.mk is the creator/initializer (which may take arguments).

8. SVG

  svg.Element.mk(<markup>);

creates an object that inherits from the prototype svg.Element, which in turn inherits from core.ObjectNode. Its content as an SVG element is as specified by the markup. At this stage, only some of the tags are supported: clipPath, circle, g, line, linearGradient, path, polygon, polyline, rect, radialGradient, stop, text, and tspan.

Here is an example:

var circle = svg.Element.mk(
'<circle fill="rgb(39, 49, 151)" stroke="black" stroke-width="1" r="5" />'
);

Each svg.Element may have an associated coordinate system/transform. The methods getTransform, and setTransform access this, and operations such as moveto affect it. See the API section for details. As in SVG, the transforms represent coordinate systems relative to parents.

At any given time, the root of the tree being displayed by ProtoPedia is held in core.root.

An svg.Element is displayed by construction of an associated element in the SVG DOM. The method Element.setDomAttribute(attr,vl) sets the given attribute of the DOM element asscociated with this to the given value. See the SVG documentation for the available attributes. Use of the setDomAttribute method is only occasionally necessary, because each svg.Element has a standard list of properties (eg stroke and fill) that are automatically transferred from the item to the DOM, by setting attributes in the DOM from the values of the item's properties of the same name.

9. Installing Prototypes

Consider this snippet of code, which can be found at /example/two_circles.js

  
core.require('/shape/circle.js',
function (circlePP) {
  let item = svg.Element.mk('<g/>');
  let circleP = core.installPrototype(circlePP); 
  // set the parameters of the circle prototype
  circleP.r = 12;
  circleP.fill = 'blue';
  let circle1 = item.add(circleP.instantiate()).show();
  let circle2 = item.add(circleP.instantiate()).show();
  circle1.moveto(Point.mk(-50,0));
  circle2.moveto(Point.mk(50,0));
  return item;
  });

Consider the state of ProtoPedia (that is, of core.root) invoked as follows

/draw.html?source=/example/two_circles.js

Here is what it looks like:

circlePP is an external component. core.installPrototype instantiates that external component, and adds its instantiation in a standard place thereby giving us an internal version circleP of the external component circlePP. That "standard place" is core.root.prototypes. installPrototype(circlePP) is nearly equivalent to

  let circleP = core.root.prototypes.add(circlePP.instantiate().hide());

but leaves out the step of automatically adding the object core.root.prototypes if it is missing.

The useful consequence is that any edits to the properties of circleP, being internal to the state, will be retained when the item is saved.

10. The Update Method

In the introductory example, interactivity is implemented "under the hood" via the ProtoPedia's graph machinery. Here is a variant which has the same behavior, ut it is implemented more directly with no dependencies on graph connections.

 
  
  item.update = function () {
    let p1=this.p1,p2 = this.p2;
    this.circle1.moveto(p1);
    this.circle2.moveto(p2);
    this.arrow1.setEnds(p1,p2);
    this.arrow2.setEnds(p2,p1);
    this.arrow1.update();
    this.arrow2.update();
  }

This method moves circle1 and circle2 to this.p1 and this.p2, respectively, and then causes the arrows to point at the circles. Updates are triggered automatically at load time, and in any circumstance of change. By "a circumstance of change" is meant one in which the implementation detects a potentially relevant event (eg editing properties in the right panel). Update methods, can, of course, be invoked explicitly. Each update method is responsible for triggering updates of its descendants (the automatic updater traverses the tree looking for update methods, but when such a method is found, it does not descend the tree further).

The code for the variant includes support for dragging in the method dragStep, which will be expla.ined in a moment.

The method

 item.initialize = function () {
   ...
 }

may be defined as well. If defined, this method is called once when the item in question is added, whether via code (core.requires), or the user interface.

11. Defining a Visual Element

In order to support resizing, an item that is intended to play the role of a visual element appearing in a catalog should follow this rule: It should define the parameters width and height, and its update method should adjust the item's SVG content to reflect these dimensions. The figure should be centered on the local origin. Examples are

/shape/rectangle.js

and

/shape/lozenge.js

In cases where the width and height are always identical, the parameter dimension should be used instead of width and height, as in

/shape/circle.js

The following lines should be included if the item you are defining is to be draggable and resizable:

 item.resizable = true;
 item.draggable = true;
12. The Graph

"Graph" here is meant in its mathematical sense: a set of vertices with edges connecting them, the Cayley D3 graph, for example. Any of the elementary items under the "shapes" tab of the standard catalog can serve as vertices. The "connectors" tab contains various kinds of edges.

ProtoPedia supplies operations for manipulating the graph structure that pertains to the vertex and edge items. The last lines of the introductory code sample illustrate one of those operations. The full list can be found in the graph section of the API.

The implementation of each kind of edge must store its ends in properties named end0, and end1

At the end of the definition of a visual element, this incantation should appear:

    
  graph.installEdgeOps(item);

Just as a visual element requires some special treatment to function as an edge, the same is true if it is to function as a vertex, though in many cases, only a single line of additional code is required. This is the case for rectangles. The line which allows a rectangle to function as a vertex is:

graph.installRectanglePeripheryOps(item);

Here is the implementation of the rectangle element. The periphery operations in question are methods that allow computation of where a ray to the center of the element will intersect its periphery. The other available primitives of this kind are:

graph.installCirclePeripheryOps(item);

and

graph.installLozengePeripheryOps(item);

Here is the underlying code at GitHub.

13. Kits

A kit is an item which incorporates specialized definitions of dragging and other behaviors. Consider the tree, in which dragging is defined a bit differently than for vertices in a generic graph (subtrees travel around with their roots), and which has specialized menu items for adding nodes. Full documentation of all of the capabilities of kits is pending. At the moment, attention is confined to dragging.

The relevant code for trees is :

item.isKit = true;



item.dragStep = function (vertex,pos) {
 let localPos = this.toLocalCoords(pos,true);
 vertex.moveto(localPos);
 /* move all the descendants of vertex to the relative
    positions they had prior to the move of vertex
 */
 this.positionvertices(vertex);
 this.update();
}

item.dragStart = function () {
// compute relative positions for all nodes (used in positionvertices)
  this.computeRelativePositions();
}


This code appears in /kit/arrowTree.js

In the initialization of a tree we have the line:

  this.vertexP.draggableInKit = true;

vertexP is the prototype for nodes in the tree.

Whenever a node defined as draggableInKit is dragged, the dragStep method of the diagram is invoked for each increment of dragging the node. Since the prototype vertexP of the vertices is defined as draggable, this property is inherited by the instances. If present, the dragStart method is called with the initial position at the start of the drag.

14. Custom Controls

Notice that when you select an arrow, little yellow boxes appear by which you can drag its head and tail around, and resize the head. These little yellow boxes are called "custom controls"

To define a custom control, the two methods needed are controlPoints(), and updateControlPoint(index,pos). The controlPoints method should return a core.ArrayNode of geom.Point (s). When the item is selected, yellow handles will appear at the positions returned by controlPoints (the points should be given relative to the item's own origin). Then, when the handles are dragged, updateControlPoint(index,pos) is called at each moment of dragging, with index set to the index of the point which generated the handle being dragged, and pos to its new position. It is the responsibility of updateControlPoint to update and redisplay the item as appropriate given the new handle position. The rounded rectangle provides an example - click on the yellow square to adjust the rounding

If all three methods are defined and adjustable is set, as is the case for the rounded rectangle, the item will be displayed with both a resize box, and the custom handles.

15. Roles, Replacement, and the Transfer State Method

In many items, the visible elements play varying roles. For example in graphs and trees, some shapes play the role of nodes/vertices, and some of edges. A role is assigned in code via:

item.role = <roleName>

When replacing an element in a diagram via "swap" or "swap prototype" in the top bar, only shapes whose role matches the role of the replaced element are presented as possibilities (via a highlight of the candidate shape as the mouse rolls over it).

For any item dest, the method dest.transferState(src, own), if present, transfers state from the replaced shape (src) to its replacement (dest). The own argument indicates whether only own properties should be transfered, or whether the operation should be applied to the relevant inherited properties as well. A common defintion of transferred state for basic shapes such as circles and rectangles is:

 

//own = consider only the own properties of src
item.transferState = function (src,own) { 
  core.setProperties(this,src,ui.stdTransferredProperties,own);
}
 
ui.stdProperties is defined in the ui module as :
 const stdTransferredProperties = ['stroke','stroke-width','fill','unselectable',
   'adjustable','draggable','draggableInKit','role','text','includeEndControls'];
 

A kit might define kit.transferElementState(dest,src,own). When a shape S is replaced by R within a kit, this kit.transferElementState(R,S,own) is called as well as R.transferState(S,own). As an example, transferElementState transfers information about the descendant relation in the case of tree kits. The own flag is set or not according to whether a prototype, or an instance is being swapped.

16. Controlling Display of Properties

When an item is selected in the structure editor, its properties and those of its prototype are displayed in the right-hand panel. If a property name begins with a double underbar (eg "__name"), it is not shown, and you can use this convention to hide properties from the user's view. But you can also employ ui.hide as exemplified by the following line from the implementation of the arrow

ui.hide(item,['head','shaft','end0','end1','direction','labelText','includeEndControls']);

The subsequent line:

item.setFieldType('solidHead','boolean');

causes the solidHead property to be displayed with a true/false selection box. Similarly, in the implementation of the shadedCircle , the line

item.setFieldType('outerFill','svg.Rgb');

causes the outerFill property to be displayed with the Spectrum color picker. By default,this chooser is deployed for all fields named fill or stroke.

By default, the values of properties are editable in the property panel. But with

ui.freeze(item,[property1,property2,...propertyn]);

the given properties are presented in non-editable form.

17. MIT License

ProtoPedia aims to support open collaboration. All of the code (and other content) at ProtoPedia, including its implementation, the catalogs of elements and diagrams, and the code that you post at the site, is covered by the MIT license, which means that the code can be freely shared and modified. See our terms and conditions for details.

18. API

(Partial listing - more to come)

Calls are given in the form f(arg1:type1,arg2:type2...) where types are: boolean, number,string, Node (core.ArrayNode or core.ObjectNode) or any When a call is described in more detail elsewhere in this document, a link is supplied.

Defaults for property values are given in parentheses just after the name of the property.

Core
Properties
core.ObjectNode.role:string
Methods
core.ObjectNode.mk()
Constructor for core.ObjectNode
core.ArrayNode.mk()
Constructor for core.ArrayNode
core.ObjectNode.set(nm:string,vl:any)
Assign vl as the child of this with name nm
core.ObjectNode.add(vl:any)
Assign vl as the child of this, with an automatically assigned name.
core.ObjectNode.remove(vl:Node)
Remove this from the tree in which it appears.
core.ObjectNode.instantiate()
core.ObjectNode.copy()
core.ObjectNode.separate()
core.ObjectNode.transferState(src:node,own:boolean)
core.ObjectNode.setFieldType(type:string)
core.stringify(v:node)
core.installPrototype(v:node)
core.lift(o)
core.setProperties(dest:node,src:node,
props:arrayOf(string))
For each property p in props, set dest[p] = src[p]
core.treeProperties( nd:Node, includeLeaves:boolean)
Returns an array of the direct properties of nd which are edges of the prototype tree (explained above).
core.forEachTreeProperty(node:node,
fn:function,includeLeaves:boolean)
An aid to traversing prototype trees. forEachTreeProperty applies the function fn to each of node's tree properties. fn should take inputs of the form (child,property,node), where node is the value passed to forEachTreeProperty, and child=node[property]
geom
geom.Point.mk(x:number,y:number):
geom.Point
Constructor for geom.Point, with properties x and y. If x and y are omitted, 0,0 are used.
geom.Point.plus(p:geom.Point):
geom.Point
Adds point p to this (vector addition)
geom.Point.minus():geom.Point
= geom.Point.mk(-this.x,-this.y)
geom.Point.difference(p:geom.Point):
geom.Point
= this.plus(p.minus())
geom.Point.times(v:number):geom.Point
Scales the point by v
geom.toPoint(v:any):geom.Point
Creates a geom.Point from several kinds of input. If v is an array [a,b] it returns a point with coordinates {x:a,y:b}; if v is a number, a point with coordinates {x:v,y:0}, if v is a geom.Point, a copy of the point.
geom.toPoint() returns the point with coordinates {x:0,y:0}.
geom.Rectangle.mk(corner:geom.Point,
extent:geom.Point):geom.Rectangle
Constructor for geom.Rectangle, with properties corner and extent. For example geom.Rectangle.mk(geom.Point.mk(10,20),geom.Point.mk(5,5)) has (10,20) as its upper-left corner, and (15,25) as its lower-right corner.
geom.Transform.mk(
translation:geom.Point,
scale:number,
rotation:number):
geom.Transform
Constructor for geom.Transform, with properties translation, scale, and rotation. Rotation is in radians. All arguments are optional, defaulting to geom.Point.mk(0,0), 1, and 0, respectively.
svg
Properties
svg.Element.__element
Methods
svg.Element.mk(s:string)
Constructor for svg Elements. s is markup.
svg.Element.hide()
Self-explanatory.
svg.Element.show()
If an element has been hidden, change its status to "visible"
svg.Element.draw()
Refresh this element. Changes to the ProtoPedia data for an element (and its descendents) are transferred to the svg model. Adding an element to the ProtoPedia tree is not reflected
svg.Element.setDomAttribute(
attr:string,vl:any)
Sets the attribute named attr of the DOM element associated with this. See the SVG documentation for the available attributes.
svg.Element.getTransform():
geom.Transform
Returns the transform of this element.
svg.Element.setTransform(tr:
geom.Transform)
Sets the transform of this element.
svg.Element.getTranslation():
geom.Point
Returns the translation of the svg transform of this element.
svg.Element.moveto(p:geom.Point)
Move this element to p. That is, set the translation of the svg transform of this element to p.
svg.Element.getScale():number
Return the scale of the transform of this element.
svg.Element.setScale(s:number)
Set the scale of the svg transform of this element to s.
svg.Element.bounds(rt:svg.Element):
geom.Rectangle
Return the bounds of the given Element (and its descendants). rt is optional. If rt is present, it should be an ancestor of this Element, and the bounds are given relative to rt's coordinate system. If rt is absent, bounds are given in the Element's own coordinate system.
graph
graph.connect1end(e:edge,
whichEnd:number,v:vertex)
connnects one end of e to v. whichEnd should be 0 or 1.
graph.connectVertices(e:edge,
v0:vertex,v1:vertex)
connnects one end of e to v0, and the other to v1.
graph.updateEnds(e:edge)
moves the ends of the edge as appropriate so that it maintains its connections
graph.graphUpdate()
updates the whole graph by updating the ends of all edges
graph.mapEndToPeriphery(e:edge,
whichEnd:number,pos:geom.Point)
Computes where to move the given end to, if that end is being dragged around the periphery
graph.installEdgeOps(n:node);
graph.installRectanglePeripheryOps(n:node);
graph.installCirclePeripheryOps(n:node);
graph.installLozengePeripheryOps(n:node);
graph.buildFromData(n:node,
v:vertex,e:edge,data:any)
adds vertices and edges instantiated from the prototypes v and ed respectively, as children of n, and as indicated by data. The Cayley D3 graph is built in this way. Here is its data. The format should be self-explanatory to those familiar with JSON.
ui
Globals
ui.whatToAdjust
In the UI, either the prototype or the instance of the selected item can be adjusted (there is a check box for telling which). This global is set to the one being adjusted.
Properties you should set
Node.unselectable:boolean (false)
If this node is clicked, its first selectable ancestor is selected
Node.adjustable:boolean (false)
A resize box appears when this node is selected.
Node.draggable:boolean (false)
Self-explanatory
Methods
ui.updateInheritors(proto:Node)
Updates and draws all of the nodes that inherit from proto. Frequently used in the form ui.updateInheritors(ui.whatToAdjust), when an edit has been made to the prototype rather instance. See the updateControlPoint method at the bottom of the implementation of the arrow.

These calls control how property values are displayed in the structure editor.

ui.hide(nd:Node,props:array of string)
ui.freeze(nd:Node,props:array of string)
Methods you define
Node.controlPoints():array of Point
Node.updateControlPoints(index:number,pos:Point)
Node.update()
Node.initialize()