4 Adding new objects
Ad5001 edited this page 2 months ago

Adding new objects

Objects are at the core of LogarithmPlotter. Extending LogarithmPlotter to add new objects is fairly simple, but requires knowledge of JavaScript OOP and it's manipulation of Canvas Context2D.
In LogarithmPlotter, mathematical expressions are handled by the Expression class in 'mathlib.js' which allow you to execute & simplify expressions at will, as well as creating a readable string to be read by the user, and an editable one that can be edited, saved and resoted.

Below "plot" coordinates are the one based on the plot, so (0,0) is at the center of the axis.
On the other hand, "canvas" coordinate are the coordinate on the canvas, so (0,0) is at the top-left of the canvas.

To create an object, create a new file javascript files in LogarithmPlotter/qml/js/objs/ with a base like this:

/**
 *  LogarithmPlotter - Create graphs with logarithm scales.
 *  Copyright (C) 2021  Ad5001
 * 
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 * 
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 * 
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

.pragma library

.import "common.js" as Common
.import "../mathlib.js" as MathLib
.import "../parameters.js" as P

You can also import other libraries like this:

  • .import "../utils.js" as Utils for string manipulation.
  • .import "../objects.js" as Objects to interact with other objects.
  • .import "../historylib.js" as HistoryLib to create and manipulate history entries.

There exists two kinds of objects:

  • Drawable (extending the Common.DrawableObject class, the most primitive kind of object)
    • Examples: Point, Text, XCursor
  • Executable (extending the Common.ExecutableObject class, like Drawable, but allows the constant computation of an y coordinate for an x one)
    • Examples: Function, Gain Bode, Phase Bode, Sequence, Repartition Function

Executable objects can be targeted by XCursors to calculate their value at a given x coordinate.

So to create a new object, choose one of the two to extend, and then create a class in objects.js extending it's class.

Methods required for your class:

  • static type() -> string
    • Returns the type of the object (should be constant, non translatable).
    • Examples: "Text", "Function"...
  • static displayType() -> string
    • Returns the (potentially translated) string for the type of this object objects of this type (used in the editor dialog, as well as the creation buttons).
    • Examples: "Texts", "Functions"...
  • static displayTypeMultiple() -> string
    • Returns the string for multiple objects of this type (used in objects list for a category of objects).
    • Examples: "Texts", "Functions"...
  • static createable() -> boolean
    • Return true if the object should be createable directly by the user via the UI.
    • Otherwise, the object will be hidden and can only be created by code from another object.
  • static properties() -> Dictionary<string propertyName, variant propertyType>
    • Returns a dictionary listing all properties of an object that can be edited in the object editor.
    • You can also add comments by starting the property name with comment and putting the comment in the value.
    • Property type can be:
      • static types: "string", "number", "boolean"
      • Expressions: "Expression"
      • Enumerations: new P.Enum("value 1", "value 2"...) displayed as comboboxes, property is set as the string value of the enum.
      • Lists: new P.List(<static type>, format = /^.+$/, label = '', forbidAdding = false)
      • Dictionaries: new P.Dictionary(valueType: <static type>, keytType: <static type>, format = /^.+$/, preKeyLabel = '', postKeyLabel = ': ', forbidAdding = false)
      • Other objects: new P.ObjectType(<object type. E.g. "Point", "ExecutableObject"...>)
  • constructor(name = null, visible = true, color = null, labelContent = 'name + value', ...other arguments)
    • Constructor of the object, should contain at least the name, visible, color, and labelContent that are passed to the super constructor.
    • You should also beaware that Expressions may be inputed as strings or numbers when restored from save, as such you should add a line in your constructor to convert them to an expression.
    • For example: if(typeof x == 'number' || typeof x == 'string') x = new MathLib.Expression(x.toString())
    • The same goes for objects, who may be inputed by just their name.
    • For example:
        if(typeof om_0 == "string") {
            // Point name or create one
            om_0 = Objects.getObjectByName(om_0, 'Point')
            if(om_0 == null) {
                // Create new point
                om_0 = createNewRegisteredObject('Point')
                om_0.name = getNewName('ω')
                om_0.color = this.color
                om_0.labelContent = 'name'
                om_0.labelPosition = this.phase.execute() >= 0 ? 'bottom' : 'top'
                history.addToHistory(new HistoryLib.CreateNewObject(om_0.name, 'Point', om_0.export()))
                labelPosition = 'below'
            }
            om_0.requiredBy.push(this)
        }
  • export() -> list
    • Returns a list of arguments that can be serialized, and will be inputed as arguments of the object construction updon restoring a save.
    • Note: the base arguments this.name, this.color & this.labelContent should be included in first as well.
    • color argument should be exported using the toString(), Expressions should be exported using toEditableString
  • getReadableString() -> string
    • Returns the string that should be displayed as help in the objects list and as the label when using the 'name + value' labelContent.
    • You can make expressions easily displayable using it's toString() method.
  • execute(x = 1) -> number?
    • Only required for ExecutableObject.
    • Returns the executed value of the object for a given x. Return null when current x cannot return a value.
  • canExecute(x = 1) -> boolean
    • Only required for ExecutableObject.
    • Returns whether x can return a value for this object.
  • simplify(x = 1) -> string
    • Only required for ExecutableObject.
    • Returns the simplified expression for a given x.
  • draw(canvas, ctx)
    • Main method for drawing the objects using the Canvas Context2D methods and the additional methods described below.
    • ExecutableObjects can draw their label if they have a labelX and labelPosition using the following code used in numerous other objects:
        var text = this.getLabel()
        ctx.font = `${canvas.textsize}px sans-serif`
        var textSize = canvas.measureText(ctx, text)
        var posX = canvas.x2px(this.labelX)
        var posY = canvas.y2px(this.execute(this.labelX))
        switch(this.labelPosition) {
            case 'above':
                canvas.drawVisibleText(ctx, text, posX-textSize.width/2, posY-textSize.height)
                break;
            case 'below':
                canvas.drawVisibleText(ctx, text, posX-textSize.width/2, posY+textSize.height)
                break;
            case 'left':
                canvas.drawVisibleText(ctx, text, posX-textSize.width, posY-textSize.height/2)
                break;
            case 'right':
                canvas.drawVisibleText(ctx, text, posX, posY-textSize.height/2)
                break;
            case 'above-left':
                canvas.drawVisibleText(ctx, text, posX-textSize.width, posY-textSize.height)
                break;
            case 'above-right':
                canvas.drawVisibleText(ctx, text, posX, posY-textSize.height)
                break;
            case 'below-left':
                canvas.drawVisibleText(ctx, text, posX-textSize.width, posY+textSize.height)
                break;
            case 'below-right':
                canvas.drawVisibleText(ctx, text, posX, posY+textSize.height)
                break;
        }
- You can also use `Function.drawFunction(canvas, ctx, expr, definitionDomain, destinationDomain, drawPoints = true, drawDash = true)` to rapidly draw a function created on the canvas.

Other optional methods:

  • update()
    • Called every time a property of the object is changed.
  • getLabel()
    • Shorthand method you can use to get your label string depending on the state of labelContent.
    • You can extend it to suit your own needs for your objects.

Registering your object

  1. Open the autoload.js file.
  2. Add the import for your object like ```.import "yourobject.js" as YO`
  3. Call the common method C.registerObject on your object like C.registerObject(YO.YourObject)

And that's it!

Additional canvas methods.

There are a few additional methods available on the canvas objects:

  • canvas.x2px(x: number) -> number
    • Converts the x coordinate of the plot to it's equivalent on the canvas (works owth with logarithmic scale and normal scale).
  • canvas.y2px(y: number) -> number
    • Converts the y coordinate of the plot to it's equivalent on the canvas.
  • canvas.px2x(px: number) -> number
    • Reverse function of x2px.
  • canvas.px2y(px: number) -> number
    • Reverse function of y2px.
  • canvas.visible(x: number, y: number) -> boolean
    • Returns true if x and y coordinate of the plot are visible on the canvas, false otherwise.
  • canvas.measureText(context: Context2D, text: string) -> {"width": number, "height": number}
    • Returns the canvas width and height of the text were it to be written on the canvas.
    • This method also allows multiline text to be tested.
  • canvas.drawVisibleText(context: Context2D, text: string, x: number, y: number) -> null
    • Draw text at the x and y of the canvas (not x and y of the plot) only if the coordinate is visible on the canvas.
    • This method also allows multiline text to be written.
  • canvas.drawLine(context: Context2D, x1: number, y1: number, x2: number, y2: number) -> null
    • Draws a line between canvas points (x1, y1) and (x2, y2).
  • canvas.drawDashedLine(context: Context2D, x1: number, y1: number, x2: number, y2: number, dashPxSize = 10) -> null
    • Draws a dashed line between canvas points (x1, y1) and (x2, y2) width one dash being dashPxSize/2.
  • canvas.drawDashedLine2(context: Context2D, x1: number, y1: number, x2: number, y2: number, dashPxSize = 10) -> null
    • Draws a dashed line between canvas points (x1, y1) and (x2, y2) width one dash being dashPxSize.