diff --git a/Adding-new-objects.md b/Adding-new-objects.md new file mode 100644 index 0000000..d3252ad --- /dev/null +++ b/Adding-new-objects.md @@ -0,0 +1,149 @@ +# 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](https://doc.qt.io/qt-5/qml-qtquick-context2d.html). +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, open the file at `LogarithmPlotter/qml/js/objects.js`. +There exists two kinds of objects: +- Drawable (extending the DrawableObject class, the most primitive kind of object) + - Examples: Point, Text, XCursor +- Executable (extending the 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 typeMultiple() -> 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` + - 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(, format = /^.+$/, label = '', forbidAdding = false)` + - Dictionaries: `new P.Dictionary(valueType: , keytType: , format = /^.+$/, preKeyLabel = '', postKeyLabel = ': ', forbidAdding = false)` + - Other objects: `new P.ObjectType()` +- `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: +```js + if(typeof om_0 == "string") { + // Point name or create one + om_0 = 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](https://doc.qt.io/qt-5/qml-qtquick-context2d.html) 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: +```js + 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() { + + +### Registering your object +Add your object type to the `types` dictionary at the bottom of the file. + - E.g: `'Point': Point` + - Upon saving and restoring a save, objects are saved/restored in the same order as the list. + - So to avoid dependance issues, when you add your object type to list, make sure it's below the ones required for it's construction. + +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. \ No newline at end of file