9 Adding new objects
Ad5001 edited this page 2022-08-17 21:41:44 +00:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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) 2022  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
 *  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 "../objects.js" as Objects to interact with other objects.
  • .import "../math/latex.js" as Latex to use LaTeX helper functions.

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"
      • Domains and ranges: "Domain"
      • 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"...>)
    • In order to allow the properties to be properly translated, you should apply the QT_TRANSLATE_NOOP macro to both property names (in the prop namespace) and comments (in the comment namespace). For example:
    static properties() {return {
        [QT_TRANSLATE_NOOP('prop','expression')]:         'Expression',
        [QT_TRANSLATE_NOOP('prop','definitionDomain')]:   'Domain',
        [QT_TRANSLATE_NOOP('prop','destinationDomain')]:  'Domain',
                                  'comment1':             QT_TRANSLATE_NOOP(
                                                              'Ex: R+* (ℝ⁺*), N (), Z-* (ℤ⁻*), ]0;1[, {3;4;5}'
        [QT_TRANSLATE_NOOP('prop','labelPosition')]:      P.Enum.Position,
        [QT_TRANSLATE_NOOP('prop','displayMode')]:        P.Enum.FunctionDisplayType,
        [QT_TRANSLATE_NOOP('prop','labelX')]:             'number',
                                  'comment2':             QT_TRANSLATE_NOOP(
                                                              'The following parameters are used when the definition domain is a non-continuous set. (Ex: , , sets like {0;3}...)'
        [QT_TRANSLATE_NOOP('prop','drawPoints')]:         'boolean',
        [QT_TRANSLATE_NOOP('prop','drawDashedLines')]:    'boolean'
  • For enums that need to be translated, an alias should be created in parameters.js, and then use the static class value in your parameter. For example:
Enum.XCursorValuePosition = new Enum(
    QT_TR_NOOP('Next to target'),
    QT_TR_NOOP('With label'),

  • 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.labelPthis.drawLabel(canvas, ctx, this.labelPosition, canvas.x2px(this.labelX), canvas.y2px(this.execute(this.labelX)))osition = this.phase.execute() >= 0 ? 'bottom' : 'top'
                history.addToHistory(new HistoryLib.CreateNewObject(om_0.name, 'Point', om_0.export()))
                labelPosition = 'below'
  • 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 displayable using its toString() method.
  • getLatexString() -> string
    • Returns the LaTeX rendered that should be displayed as label on the canvas when using the 'name + value' labelContent.
    • You can turn expressions and sets into LaTeX by using their latexMarkup properties.
    • Similarly, variables should always be parsed with the helper function Latex.variable in order to render special characters (greek letters, sub and supscript numbers...)
  • 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 the drawLabel object function:
        this.drawLabel(canvas, ctx, this.labelPosition, canvas.x2px(this.labelX), canvas.y2px(this.execute(this.labelX)))
- 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.