Reorganizing paths

This commit is contained in:
Adsooi 2024-09-30 00:23:39 +02:00
parent e9d204daab
commit 34cb856dd4
Signed by: Ad5001
GPG key ID: EF45F9C6AFE20160
249 changed files with 118 additions and 294 deletions

View file

@ -0,0 +1,57 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 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/>.
*/
import Objects from "../module/objects.mjs"
import { DrawableObject } from "./common.mjs"
import Point from "./point.mjs"
import Text from "./text.mjs"
import Function from "./function.mjs"
import BodeMagnitude from "./bodemagnitude.mjs"
import BodePhase from "./bodephase.mjs"
import BodeMagnitudeSum from "./bodemagnitudesum.mjs"
import BodePhaseSum from "./bodephasesum.mjs"
import XCursor from "./xcursor.mjs"
import Sequence from "./sequence.mjs"
import DistributionFunction from "./distribution.mjs"
/**
* Registers the object obj in the object list.
* @param {DrawableObject} obj - Object to be registered.
*/
function registerObject(obj) {
// Registers an object to be used in LogarithmPlotter.
if(obj.prototype instanceof DrawableObject) {
if(!Objects.types[obj.type()])
Objects.types[obj.type()] = obj
} else {
console.error("Could not register object " + (obj?.type() ?? obj.constructor.name) + ", as it isn't a DrawableObject.")
}
}
if(Object.keys(Objects.types).length === 0) {
registerObject(Point)
registerObject(Text)
registerObject(Function)
registerObject(BodeMagnitude)
registerObject(BodePhase)
registerObject(BodeMagnitudeSum)
registerObject(BodePhaseSum)
registerObject(XCursor)
registerObject(Sequence)
registerObject(DistributionFunction)
}

View file

@ -0,0 +1,162 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 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/>.
*/
import { parseDomain, executeExpression, Expression, EmptySet, Domain } from "../math/index.mjs"
import { CreateNewObject } from "../history/index.mjs"
import * as P from "../parameters.mjs"
import Objects from "../module/objects.mjs"
import Latex from "../module/latex.mjs"
import History from "../module/history.mjs"
import { ExecutableObject } from "./common.mjs"
import Function from "./function.mjs"
export default class BodeMagnitude extends ExecutableObject {
static type() {
return "Gain Bode"
}
static displayType() {
return qsTranslate("bodemagnitude", "Bode Magnitude")
}
static displayTypeMultiple() {
return qsTranslate("bodemagnitude", "Bode Magnitudes")
}
static properties() {
return {
[QT_TRANSLATE_NOOP("prop", "om_0")]: new P.ObjectType("Point"),
[QT_TRANSLATE_NOOP("prop", "pass")]: P.Enum.BodePass,
[QT_TRANSLATE_NOOP("prop", "gain")]: new P.Expression(),
[QT_TRANSLATE_NOOP("prop", "labelPosition")]: P.Enum.Position,
[QT_TRANSLATE_NOOP("prop", "labelX")]: "number",
[QT_TRANSLATE_NOOP("prop", "omGraduation")]: "boolean"
}
}
constructor(name = null, visible = true, color = null, labelContent = "name + value",
om_0 = "", pass = "high", gain = "20", labelPosition = "above", labelX = 1, omGraduation = false) {
if(name == null) name = Objects.getNewName("G")
if(name === "G") name = "G₀" // G is reserved for sum of BODE magnitudes (Somme gains Bode).
super(name, visible, color, labelContent)
if(typeof om_0 == "string") {
// Point name or create one
om_0 = Objects.currentObjectsByName[om_0]
if(om_0 == null) {
// Create new point
om_0 = Objects.createNewRegisteredObject("Point", [Objects.getNewName("ω"), true, this.color, "name"])
History.addToHistory(new CreateNewObject(om_0.name, "Point", om_0.export()))
om_0.update()
labelPosition = "below"
}
om_0.requiredBy.push(this)
}
/** @type {Point} */
this.om_0 = om_0
this.pass = pass
if(typeof gain == "number" || typeof gain == "string") gain = new Expression(gain.toString())
this.gain = gain
this.labelPosition = labelPosition
this.labelX = labelX
this.omGraduation = omGraduation
}
getReadableString() {
let pass = this.pass === "low" ? qsTranslate("bodemagnitude", "low-pass") : qsTranslate("bodemagnitude", "high-pass")
return `${this.name}: ${pass}; ${this.om_0.name} = ${this.om_0.x}\n ${" ".repeat(this.name.length)}${this.gain.toString(true)} dB/dec`
}
getLatexString() {
let pass = this.pass === "low" ? qsTranslate("bodemagnitude", "low-pass") : qsTranslate("bodemagnitude", "high-pass")
return `\\mathrm{${Latex.variable(this.name)}:}\\begin{array}{l}
\\textsf{${pass}};${Latex.variable(this.om_0.name)} = ${this.om_0.x.latexMarkup} \\\\
${this.gain.latexMarkup}\\textsf{ dB/dec}
\\end{array}`
}
execute(x = 1) {
if(typeof x == "string") x = executeExpression(x)
if((this.pass === "high" && x < this.om_0.x) || (this.pass === "low" && x > this.om_0.x)) {
let dbfn = new Expression(`${this.gain.execute()}*(ln(x)-ln(${this.om_0.x}))/ln(10)+${this.om_0.y}`)
return dbfn.execute(x)
} else {
return this.om_0.y.execute()
}
}
simplify(x = 1) {
let xval = x
if(typeof x == "string") xval = executeExpression(x)
if((this.pass === "high" && xval < this.om_0.x.execute()) || (this.pass === "low" && xval > this.om_0.x.execute())) {
let dbfn = new Expression(`${this.gain.execute()}*(ln(x)-ln(${this.om_0.x}))/ln(10)+${this.om_0.y}`)
return dbfn.simplify(x)
} else {
return this.om_0.y.toString()
}
}
canExecute(x = 1) {
return true
}
draw(canvas) {
let base = [canvas.x2px(this.om_0.x.execute()), canvas.y2px(this.om_0.y.execute())]
let dbfn = new Expression(`${this.gain.execute()}*(log10(x)-log10(${this.om_0.x}))+${this.om_0.y}`)
let inDrawDom
if(this.pass === "high") {
// High pass, linear line from beginning, then constant to the end.
canvas.drawLine(base[0], base[1], canvas.width, base[1])
inDrawDom = parseDomain(`]-inf;${this.om_0.x}[`)
} else {
// Low pass, constant from the beginning, linear line to the end.
canvas.drawLine(base[0], base[1], 0, base[1])
inDrawDom = parseDomain(`]${this.om_0.x};+inf[`)
}
Function.drawFunction(canvas, dbfn, inDrawDom, Domain.R)
// Dashed line representing break in function
let xpos = canvas.x2px(this.om_0.x.execute())
let dashPxSize = 10
for(let i = 0; i < canvas.height && this.omGraduation; i += dashPxSize * 2)
canvas.drawLine(xpos, i, xpos, i + dashPxSize)
// Label
this.drawLabel(canvas, this.labelPosition, canvas.x2px(this.labelX), canvas.y2px(this.execute(this.labelX)))
}
update() {
super.update()
/** @type {BodeMagnitudeSum[]} */
let sumObjs = Objects.currentObjects["Somme gains Bode"]
if(sumObjs !== undefined && sumObjs.length > 0) {
sumObjs[0].recalculateCache()
} else {
Objects.createNewRegisteredObject("Somme gains Bode")
}
}
delete() {
super.delete()
/** @type {BodeMagnitudeSum[]} */
let sumObjs = Objects.currentObjects["Somme gains Bode"]
if(sumObjs !== undefined && sumObjs.length > 0) {
sumObjs[0].recalculateCache()
}
}
}

View file

@ -0,0 +1,158 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 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/>.
*/
import { Range, Expression, Domain } from "../math/index.mjs"
import * as P from "../parameters.mjs"
import Objects from "../module/objects.mjs"
import Latex from "../module/latex.mjs"
import { ExecutableObject } from "./common.mjs"
import Function from "./function.mjs"
export default class BodeMagnitudeSum extends ExecutableObject {
static type() {
return "Somme gains Bode"
}
static displayType() {
return qsTranslate("bodemagnitudesum", "Bode Magnitudes Sum")
}
static displayTypeMultiple() {
return qsTranslate("bodemagnitudesum", "Bode Magnitudes Sum")
}
static createable() {
return false
}
static properties() {
return {
[QT_TRANSLATE_NOOP("prop", "labelPosition")]: P.Enum.Position,
[QT_TRANSLATE_NOOP("prop", "labelX")]: "number"
}
}
constructor(name = null, visible = true, color = null, labelContent = "name + value",
labelPosition = "above", labelX = 1) {
if(name == null) name = "G"
super(name, visible, color, labelContent)
this.labelPosition = labelPosition
this.labelX = labelX
this.recalculateCache()
}
getReadableString() {
return `${this.name} = ${Objects.getObjectsName("Gain Bode").join(" + ")}`
}
getLatexString() {
return `${Latex.variable(this.name)} = ${Objects.getObjectsName("Gain Bode").map(name => Latex.variable(name)).join(" + ")}`
}
execute(x = 0) {
for(let [limitedDrawFunction, inDrawDom] of this.cachedParts) {
if(inDrawDom.includes(x)) {
return limitedDrawFunction.execute(x)
}
}
return null
}
canExecute(x = 1) {
return true // Should always be true
}
simplify(x = 1) {
for(let [limitedDrawFunction, inDrawDom] of this.cachedParts) {
if(inDrawDom.includes(x)) {
return limitedDrawFunction.simplify(x)
}
}
return ""
}
recalculateCache() {
this.cachedParts = []
// Calculating this is fairly resource expansive so it's cached.
let magnitudeObjects = Objects.currentObjects["Gain Bode"]
if(magnitudeObjects === undefined || magnitudeObjects.length < 1) {
Objects.deleteObject(this.name)
} else {
console.log("Recalculating cache gain")
// Minimum to draw (can be expended if needed, just not infinite or it'll cause issues.
const MIN_DRAW = 1e-20
// Format: [[x value of where the filter transitions, magnitude, high-pass (bool)]]
const magnitudes = []
const XVALUE = 0
const MAGNITUDE = 1
const PASS = 2
magnitudes.push([Number.MAX_VALUE, 0, true]) // Draw the ending section
// Collect data from current magnitude (or gain in French) objects.
let baseY = 0
for(/** @type {BodeMagnitude} */ let magnitudeObj of magnitudeObjects) { // Sorting by their om_0 position.
const om0x = magnitudeObj.om_0.x.execute()
magnitudes.push([om0x, magnitudeObj.gain.execute(), magnitudeObj.pass === "high"])
baseY += magnitudeObj.execute(MIN_DRAW)
}
// Sorting the data by their x transitions value
magnitudes.sort((a, b) => a[XVALUE] - b[XVALUE])
// Adding the total gains.
let magnitudesBeforeTransition = []
let magnitudesAfterTransition = []
let totalMagnitudeAtStart = 0 // Magnitude at the lowest x value (sum of all high-pass magnitudes)
for(let [om0x, magnitude, highpass] of magnitudes) {
if(highpass) {
magnitudesBeforeTransition.push(magnitude)
magnitudesAfterTransition.push(0)
totalMagnitudeAtStart += magnitude
} else {
magnitudesBeforeTransition.push(0)
magnitudesAfterTransition.push(magnitude)
}
}
// Calculating parts
let previousTransitionX = MIN_DRAW
let currentMagnitude = totalMagnitudeAtStart
for(let transitionID = 0; transitionID < magnitudes.length; transitionID++) {
const transitionX = magnitudes[transitionID][XVALUE]
// Create draw function that will be used during drawing.
const limitedDrawFunction = new Expression(`${currentMagnitude}*(ln(x)-ln(${previousTransitionX}))/ln(10)+${baseY}`)
const drawDomain = new Range(previousTransitionX, transitionX, true, false)
this.cachedParts.push([limitedDrawFunction, drawDomain])
// Prepare default values for next function.
previousTransitionX = transitionX
baseY = limitedDrawFunction.execute(transitionX)
currentMagnitude += magnitudesAfterTransition[transitionID] - magnitudesBeforeTransition[transitionID]
}
}
}
draw(canvas) {
if(this.cachedParts.length > 0) {
for(let [limitedDrawFunction, drawDomain] of this.cachedParts) {
Function.drawFunction(canvas, limitedDrawFunction, drawDomain, Domain.R)
// Check if necessary to draw label
if(drawDomain.includes(this.labelX)) {
this.drawLabel(canvas, this.labelPosition, canvas.x2px(this.labelX), canvas.y2px(this.execute(this.labelX)))
}
}
}
}
}

View file

@ -0,0 +1,147 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 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/>.
*/
import { executeExpression, Expression } from "../math/index.mjs"
import { CreateNewObject } from "../history/index.mjs"
import * as P from "../parameters.mjs"
import Objects from "../module/objects.mjs"
import History from "../module/history.mjs"
import Latex from "../module/latex.mjs"
import { ExecutableObject } from "./common.mjs"
export default class BodePhase extends ExecutableObject {
static type() {
return "Phase Bode"
}
static displayType() {
return qsTranslate("bodephase", "Bode Phase")
}
static displayTypeMultiple() {
return qsTranslate("bodephase", "Bode Phases")
}
static properties() {
return {
[QT_TRANSLATE_NOOP("prop", "om_0")]: new P.ObjectType("Point"),
[QT_TRANSLATE_NOOP("prop", "phase")]: new P.Expression(),
[QT_TRANSLATE_NOOP("prop", "unit")]: new P.Enum("°", "deg", "rad"),
[QT_TRANSLATE_NOOP("prop", "labelPosition")]: P.Enum.Position,
[QT_TRANSLATE_NOOP("prop", "labelX")]: "number"
}
}
constructor(name = null, visible = true, color = null, labelContent = "name + value",
om_0 = "", phase = 90, unit = "°", labelPosition = "above", labelX = 1) {
if(name == null) name = Objects.getNewName("φ")
if(name === "φ") name = "φ₀" // φ is reserved for sum of Bode phases.
super(name, visible, color, labelContent)
if(typeof phase == "number" || typeof phase == "string") phase = new Expression(phase.toString())
this.phase = phase
if(typeof om_0 == "string") {
// Point name or create one
om_0 = Objects.currentObjectsByName[om_0]
if(om_0 == null) {
// Create new point
om_0 = Objects.createNewRegisteredObject("Point", [Objects.getNewName("ω"), this.color, "name"])
om_0.labelPosition = this.phase.execute() >= 0 ? "above" : "below"
History.history.addToHistory(new CreateNewObject(om_0.name, "Point", om_0.export()))
labelPosition = "below"
}
om_0.requiredBy.push(this)
}
/** @type {Point} */
this.om_0 = om_0
this.unit = unit
this.labelPosition = labelPosition
this.labelX = labelX
}
getReadableString() {
return `${this.name}: ${this.phase.toString(true)}${this.unit} at ${this.om_0.name} = ${this.om_0.x}`
}
getLatexString() {
return `${Latex.variable(this.name)}: ${this.phase.latexMarkup}\\textsf{${this.unit} at }${Latex.variable(this.om_0.name)} = ${this.om_0.x.latexMarkup}`
}
execute(x = 1) {
if(typeof x == "string") x = executeExpression(x)
if(x < this.om_0.x) {
return this.om_0.y.execute()
} else {
return this.om_0.y.execute() + this.phase.execute()
}
}
simplify(x = 1) {
let xval = x
if(typeof x == "string") xval = executeExpression(x)
if(xval < this.om_0.x) {
return this.om_0.y.toString()
} else {
let newExp = this.om_0.y.toEditableString() + " + " + this.phase.toEditableString()
return new Expression(newExp)
}
}
canExecute(x = 1) {
return true
}
draw(canvas) {
let baseX = canvas.x2px(this.om_0.x.execute())
let omy = this.om_0.y.execute()
let augmt = this.phase.execute()
let baseY = canvas.y2px(omy)
let augmtY = canvas.y2px(omy + augmt)
// Before change line.
canvas.drawLine(0, baseY, Math.min(baseX, canvas.height), baseY)
// Transition line.
canvas.drawLine(baseX, baseY, baseX, augmtY)
// After change line
canvas.drawLine(Math.max(0, baseX), augmtY, canvas.width, augmtY)
// Label
this.drawLabel(canvas, this.labelPosition, canvas.x2px(this.labelX), canvas.y2px(this.execute(this.labelX)))
}
update() {
super.update()
/** @type {BodePhaseSum[]} */
let sumObjs = Objects.currentObjects["Somme phases Bode"]
if(sumObjs !== undefined && sumObjs.length > 0) {
sumObjs[0].recalculateCache()
} else {
Objects.createNewRegisteredObject("Somme phases Bode")
}
}
delete() {
super.update()
/** @type {BodePhaseSum[]} */
let sumObjs = Objects.currentObjects["Somme phases Bode"]
if(sumObjs !== undefined && sumObjs.length > 0) {
sumObjs[0].recalculateCache()
}
}
}

View file

@ -0,0 +1,139 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 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/>.
*/
import { executeExpression, Expression } from "../math/index.mjs"
import * as P from "../parameters.mjs"
import Objects from "../module/objects.mjs"
import Latex from "../module/latex.mjs"
import { ExecutableObject } from "./common.mjs"
export default class BodePhaseSum extends ExecutableObject {
static type() {
return "Somme phases Bode"
}
static displayType() {
return qsTranslate("bodephasesum", "Bode Phases Sum")
}
static displayTypeMultiple() {
return qsTranslate("bodephasesum", "Bode Phases Sum")
}
static createable() {
return false
}
static properties() {
return {
[QT_TRANSLATE_NOOP("prop", "labelPosition")]: P.Enum.Position,
[QT_TRANSLATE_NOOP("prop", "labelX")]: "number"
}
}
constructor(name = null, visible = true, color = null, labelContent = "name + value",
labelPosition = "above", labelX = 1) {
if(name == null) name = "φ"
super(name, visible, color, labelContent)
this.labelPosition = labelPosition
this.labelX = labelX
this.recalculateCache()
}
getReadableString() {
return `${this.name} = ${Objects.getObjectsName("Phase Bode").join(" + ")}`
}
getLatexString() {
return `${Latex.variable(this.name)} = ${Objects.getObjectsName("Phase Bode").map(name => Latex.variable(name)).join(" + ")}`
}
execute(x = 1) {
if(typeof x == "string") x = executeExpression(x)
for(let i = 0; i < this.om0xList.length - 1; i++) {
if(x >= this.om0xList[i] && x < this.om0xList[i + 1]) return this.phasesList[i]
}
}
simplify(x = 1) {
let xval = x
if(typeof x == "string") xval = executeExpression(x)
for(let i = 0; i < this.om0xList.length - 1; i++) {
if(xval >= this.om0xList[i] && xval < this.om0xList[i + 1]) {
return (new Expression(this.phasesExprList[i])).simplify()
}
}
return "0"
}
canExecute(x = 1) {
return true
}
recalculateCache() {
// Minimum to draw (can be expended if needed, just not infinite or it'll cause issues.
let drawMin = 1e-20
let drawMax = 1e20
this.om0xList = [drawMin, drawMax]
this.phasesList = [0]
this.phasesExprList = ["0"]
let phasesDict = new Map()
let phasesExprDict = new Map()
phasesDict.set(drawMax, 0)
let phaseObjects = Objects.currentObjects["Phase Bode"]
if(phaseObjects === undefined || phaseObjects.length < 1) {
Objects.deleteObject(this.name)
} else {
console.log("Recalculating cache phase")
for(/** @type {BodePhase} */ let obj of phaseObjects) {
this.om0xList.push(obj.om_0.x.execute())
if(!phasesDict.has(obj.om_0.x.execute())) {
phasesDict.set(obj.om_0.x.execute(), obj.phase.execute())
phasesExprDict.set(obj.om_0.x.execute(), obj.phase.toEditableString())
} else {
phasesDict.set(obj.om_0.x.execute(), obj.phase.execute() + phasesDict.get(obj.om_0.x.execute()))
phasesExprDict.set(obj.om_0.x.execute(), obj.phase.toEditableString() + "+" + phasesExprDict.get(obj.om_0.x.execute()))
}
this.phasesList[0] += obj.om_0.y.execute()
this.phasesExprList[0] += "+" + obj.om_0.y.toEditableString()
}
this.om0xList.sort((a, b) => a - b)
for(let i = 1; i < this.om0xList.length; i++) {
this.phasesList[i] = this.phasesList[i - 1] + phasesDict.get(this.om0xList[i])
this.phasesExprList[i] = this.phasesExprList[i - 1] + "+" + phasesDict.get(this.om0xList[i])
}
}
}
draw(canvas) {
for(let i = 0; i < this.om0xList.length - 1; i++) {
let om0xBegin = canvas.x2px(this.om0xList[i])
let om0xEnd = canvas.x2px(this.om0xList[i + 1])
let baseY = canvas.y2px(this.phasesList[i])
let nextY = canvas.y2px(this.phasesList[i + 1])
canvas.drawLine(om0xBegin, baseY, om0xEnd, baseY)
canvas.drawLine(om0xEnd, baseY, om0xEnd, nextY)
}
// Label
this.drawLabel(canvas, this.labelPosition, canvas.x2px(this.labelX), canvas.y2px(this.execute(this.labelX)))
}
}

418
common/src/objs/common.mjs Normal file
View file

@ -0,0 +1,418 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 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/>.
*/
import { getRandomColor } from "../utils.mjs"
import Objects from "../module/objects.mjs"
import Latex from "../module/latex.mjs"
import { ensureTypeSafety, serializesByPropertyType } from "../parameters.mjs"
// This file contains the default data to be imported from all other objects
/**
* Class to extend for every type of object that
* can be drawn on the canvas.
*/
export class DrawableObject {
/**
* Base name of the object. Needs to be constant over time.
* @return {string} Type of the object.
*/
static type() {
return "Unknown"
}
/**
* Translated name of the object to be shown to the user.
* @return {string}
*/
static displayType() {
return "Unknown"
}
/**
* Translated name of the object in plural form to be shown to the user.
* @return {string}
*/
static displayTypeMultiple() {
return "Unknowns"
}
/**
* True if this object can be created by the user, false if it can only
* be instantiated by other objects
* @return {boolean}
*/
static createable() {
return true
}
/**
* List of properties used in the Object Editor.
*
* Properties are set with key as property name and
* value as it's type name (e.g 'numbers', 'string'...),
* an Enum for enumerations, an ObjectType for DrawableObjects
* with a specific type, a List instance for lists, a
* Dictionary instance for dictionaries, an Expression for expressions...
*
* If the property is to be translated, the key should be passed
* through the QT_TRANSLATE_NOOP macro in that form:
* [QT_TRANSLATE_NOOP('prop','key')]
* Enums that are translated should be indexed in parameters.mjs and
* then be linked directly here.
*
* @return {Object.<string,string|Enum|List|ObjectType|Dictionary>}
*/
static properties() {
return {}
}
/**
* True if this object can be executed, so that an y value might be computed
* for an x value. Only ExecutableObjects have that property set to true.
* @return {boolean}
*/
static executable() {
return false
}
/**
* Imports the object from its serialized form.
* @return {DrawableObject}
*/
static import(name, visible, color, labelContent, ...args) {
let importedArgs = [name.toString(), visible === true, color.toString(), labelContent]
console.log("Importing", this.type(), name, args)
for(let [name, propType] of Object.entries(this.properties()))
if(!name.startsWith("comment")) {
importedArgs.push(ensureTypeSafety(propType, args[importedArgs.length - 4]))
}
return new this(...importedArgs)
}
/**
* Base constructor for the object.
* @param {string} name - Name of the object
* @param {boolean} visible - true if the object is visible, false otherwise.
* @param {color|string} color - Color of the object (can be string or QColor)
* @param {"null"|"name"|"name + value"} labelContent - One of 'null', 'name' or 'name + value' describing the content of the label.
* @constructor
*/
constructor(name, visible = true, color = null, labelContent = "name + value") {
if(color == null) color = getRandomColor()
this.type = this.constructor.type()
this.name = name
this.visible = visible
this.color = color
this.labelContent = labelContent // "null", "name", "name + value"
this.requiredBy = []
this.requires = []
}
/**
* Serializes the object in an array that can be JSON serialized.
* These parameters will be re-entered in the constructor when restored.
* @return {array}
*/
export() {
let exportList = [this.name, this.visible, this.color.toString(), this.labelContent]
for(let [name, propType] of Object.entries(this.constructor.properties()))
if(!name.startsWith("comment"))
exportList.push(serializesByPropertyType(propType, this[name]))
return exportList
}
/**
* String representing the object that will be displayed to the user.
* It allows for 2 lines and translated text, but it shouldn't be too long.
* @return {string}
*/
getReadableString() {
return `${this.name} = Unknown`
}
/**
* Latex markuped version of the readable string.
* Every non latin character should be passed as latex symbols and formulas
* should be in latex form.
* See ../latex.mjs for helper methods.
* @return {string}
*/
getLatexString() {
return this.getReadableString()
}
/**
* Readable string content of the label depending on the value of the latexContent.
* @return {string}
*/
getLabel() {
switch(this.labelContent) {
case "name":
return this.name
case "name + value":
return this.getReadableString()
case "null":
return ""
}
}
/**
* Latex markup string content of the label depending on the value of the latexContent.
* Every non latin character should be passed as latex symbols and formulas
* should be in latex form.
* See ../latex.mjs for helper methods.
* @return {string}
*/
getLatexLabel() {
switch(this.labelContent) {
case "name":
return Latex.variable(this.name)
case "name + value":
return this.getLatexString()
case "null":
return ""
}
}
/**
* Returns the recursive list of objects this one depends on.
* @return {array}
*/
getDependenciesList() {
let dependencies = this.requires.map(obj => obj)
for(let obj of this.requires)
dependencies = dependencies.concat(obj.getDependenciesList())
return dependencies
}
/**
* Callback method when one of the properties of the object is updated.
*/
update() {
// Refreshing dependencies.
for(let obj of this.requires)
obj.requiredBy = obj.requiredBy.filter(dep => dep !== this)
// Checking objects this one depends on
this.requires = []
let currentObjectsByName = Objects.currentObjectsByName
let properties = this.constructor.properties()
for(let property in properties)
if(typeof properties[property] == "object" && "type" in properties[property])
if(properties[property].type === "Expression" && this[property] != null) {
// Expressions with dependencies
for(let objName of this[property].requiredObjects()) {
if(objName in currentObjectsByName && !this.requires.includes(currentObjectsByName[objName])) {
this.requires.push(currentObjectsByName[objName])
currentObjectsByName[objName].requiredBy.push(this)
}
}
if(this[property].cached && this[property].requiredObjects().length > 0)
// Recalculate
this[property].recache()
} else if(properties[property].type === "ObjectType" && this[property] != null) {
// Object dependency
this.requires.push(this[property])
this[property].requiredBy.push(this)
}
// Updating objects dependent on this one
for(let req of this.requiredBy)
req.update()
}
/**
* Callback method when the object is about to get deleted.
*/
delete() {
for(let toRemove of this.requiredBy) { // Normally, there should be none here, but better leave nothing just in case.
Objects.deleteObject(toRemove.name)
}
for(let toRemoveFrom of this.requires) {
toRemoveFrom.requiredBy = toRemoveFrom.requiredBy.filter(o => o !== this)
}
}
/**
* Abstract method. Draw the object onto the canvas with the.
* @param {CanvasAPI} canvas
*/
draw(canvas) {
}
/**
* Applicates a drawFunction with two position arguments depending on
* both the posX and posY of where the label should be displayed,
* and the labelPosition which declares the label should be displayed
* relatively to that position.
*
* @param {string|Enum} labelPosition - Position of the label relative to the marked position
* @param {number} offset - Margin between the position and the object to be drawn
* @param {{width: number, height: number}} size - Size of the label item, containing two properties, "width" and "height"
* @param {number} posX - Component of the marked position on the x-axis
* @param {number} posY - Component of the marked position on the y-axis
* @param {function} drawFunction - Function with two arguments (x, y) that will be called to draw the label
*/
drawPositionDivergence(labelPosition, offset, size, posX, posY, drawFunction) {
switch(labelPosition) {
case "center":
drawFunction(posX - size.width / 2, posY - size.height / 2)
break
case "top":
case "above":
drawFunction(posX - size.width / 2, posY - size.height - offset)
break
case "bottom":
case "below":
drawFunction(posX - size.width / 2, posY + offset)
break
case "left":
drawFunction(posX - size.width - offset, posY - size.height / 2)
break
case "right":
drawFunction(posX + offset, posY - size.height / 2)
break
case "top-left":
case "above-left":
drawFunction(posX - size.width, posY - size.height - offset)
break
case "top-right":
case "above-right":
drawFunction(posX + offset, posY - size.height - offset)
break
case "bottom-left":
case "below-left":
drawFunction(posX - size.width - offset, posY + offset)
break
case "bottom-right":
case "below-right":
drawFunction(posX + offset, posY + offset)
break
}
}
/**
* Automatically draw text (by default the label of the object on the canvas
* depending on user settings.
* This method takes into account both the posX and posY of where the label
* should be displayed, including the labelPosition relative to it.
* The text is get both through the getLatexFunction and getTextFunction
* depending on whether to use latex.
* Then, it's displayed using the drawFunctionLatex (x,y,imageData) and
* drawFunctionText (x,y,text) depending on whether to use latex.
*
* @param {CanvasAPI} canvas
* @param {string|Enum} labelPosition - Position of the label relative to the marked position
* @param {number} posX - Component of the marked position on the x-axis
* @param {number} posY - Component of the marked position on the y-axis
* @param {boolean} forceText - Force the rendering of the label as text
* @param {function|null} getLatexFunction - Function (no argument) to get the latex markup to be displayed
* @param {function|null} getTextFunction - Function (no argument) to get the text to be displayed
* @param {function|null} drawFunctionLatex - Function (x,y,imageData) to display the latex image
* @param {function|null} drawFunctionText - Function (x,y,text,textSize) to display the text
*/
drawLabel(canvas, labelPosition, posX, posY, forceText = false,
getLatexFunction = null, getTextFunction = null, drawFunctionLatex = null, drawFunctionText = null) {
// Default functions
if(getLatexFunction == null)
getLatexFunction = this.getLatexLabel.bind(this)
if(getTextFunction == null)
getTextFunction = this.getLabel.bind(this)
if(drawFunctionLatex == null)
drawFunctionLatex = (x, y, ltxImg) => canvas.drawVisibleImage(ltxImg.source, x, y, ltxImg.width, ltxImg.height)
if(drawFunctionText == null)
drawFunctionText = (x, y, text, textSize) => canvas.drawVisibleText(text, x, y + textSize.height) // Positioned from left bottom
// Drawing the label
if(!forceText && Latex.enabled) {
// With latex
let drawLblCb = ((ltxImg) => {
this.drawPositionDivergence(labelPosition, 8 + canvas.linewidth / 2, ltxImg, posX, posY, (x, y) => drawFunctionLatex(x, y, ltxImg))
})
let ltxLabel = getLatexFunction()
if(ltxLabel !== "")
canvas.renderLatexImage(ltxLabel, this.color, drawLblCb)
} else {
// Without latex
let text = getTextFunction()
canvas.font = `${canvas.textsize}px sans-serif`
let textSize = canvas.measureText(text)
this.drawPositionDivergence(labelPosition, 8 + canvas.linewidth / 2, textSize, posX, posY, (x, y) => drawFunctionText(x, y, text, textSize))
}
}
toString() {
return this.name
}
}
/**
* Class to be extended for every object on which
* an y for a x can be computed with the execute function.
* If a value cannot be found during execute, it will
* return null. However, theses same x values will
* return false when passed to canExecute.
*/
export class ExecutableObject extends DrawableObject {
/**
* Returns the corresponding y value for an x value.
* If the object isn't defined on the given x, then
* this function will return null.
*
* @param {number} x
* @returns {number|null}
*/
execute(x = 1) {
return 0
}
/**
* Returns false if the object isn't defined on the given x, true otherwise.
*
* @param {number} x
* @returns {boolean}
*/
canExecute(x = 1) {
return true
}
/**
* Returns the simplified expression string for a given x.
*
* @param {number} x
* @returns {string|Expression}
*/
simplify(x = 1) {
return "0"
}
/**
* True if this object can be executed, so that an y value might be computed
* for an x value. Only ExecutableObjects have that property set to true.
* @return {boolean}
*/
static executable() {
return true
}
}

View file

@ -0,0 +1,159 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 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/>.
*/
import * as P from "../parameters.mjs"
import Objects from "../module/objects.mjs"
import Latex from "../module/latex.mjs"
import { ExecutableObject } from "./common.mjs"
export default class DistributionFunction extends ExecutableObject {
static type() {
return "Repartition"
}
static displayType() {
return qsTranslate("distribution", "Repartition")
}
static displayTypeMultiple() {
return qsTranslate("distribution", "Repartition functions")
}
static properties() {
return {
"comment1": QT_TRANSLATE_NOOP(
"comment",
"Note: Specify the probability for each value."
),
[QT_TRANSLATE_NOOP("prop", "probabilities")]: new P.Dictionary("string", "double", /^-?[\d.,]+$/, /^-?[\d.,]+$/, "P({name_} = ", ") = "),
[QT_TRANSLATE_NOOP("prop", "labelPosition")]: P.Enum.Position,
[QT_TRANSLATE_NOOP("prop", "labelX")]: "number"
}
}
static import(name, visible, color, labelContent, ...args) {
console.log(args, args.length)
if(args.length === 5) {
// Two legacy values no longer included.
args.shift()
args.shift()
}
return super.import(name, visible, color, labelContent, ...args)
}
constructor(name = null, visible = true, color = null, labelContent = "name + value",
probabilities = { "0": "0" }, labelPosition = "above", labelX = 1) {
if(name == null) name = Objects.getNewName("XYZUVW", "F_")
super(name, visible, color, labelContent)
this.probabilities = probabilities
this.labelPosition = labelPosition
this.labelX = labelX
this.update()
}
getReadableString() {
let keys = Object.keys(this.probabilities).sort((a, b) => a - b)
let varname = this.name.substring(this.name.indexOf("_") + 1)
return `${this.name}(x) = P(${varname} ≤ x)\n` + keys.map(idx => `P(${varname}=${idx})=${this.probabilities[idx]}`).join("; ")
}
getLatexString() {
let keys = Object.keys(this.probabilities).sort((a, b) => a - b)
let funcName = Latex.variable(this.name)
let varName = Latex.variable(this.name.substring(this.name.indexOf("_") + 1))
return `\\begin{array}{l}{${funcName}}(x) = P(${varName} \\le x)\\\\` + keys.map(idx => `P(${varName}=${idx})=${this.probabilities[idx]}`).join("; ") + "\\end{array}"
}
execute(x = 1) {
let ret = 0
Object.keys(this.probabilities).sort((a, b) => a - b).forEach(idx => {
if(x >= idx) ret += parseFloat(this.probabilities[idx].replace(/,/g, "."))
})
return ret
}
canExecute(x = 1) {
return true
}
// Simplify returns the simplified string of the expression.
simplify(x = 1) {
return this.execute(x).toString()
}
getLabel() {
switch(this.labelContent) {
case "name":
return `${this.name}(x)`
case "name + value":
return this.getReadableString()
case "null":
return ""
}
}
draw(canvas) {
let currentY = 0
let keys = Object.keys(this.probabilities).map(idx => parseInt(idx)).sort((a, b) => a - b)
if(canvas.isVisible(keys[0], this.probabilities[keys[0]].replace(/,/g, "."))) {
canvas.drawLine(0, canvas.y2px(0), canvas.x2px(keys[0]), canvas.y2px(0))
if(canvas.isVisible(keys[0], 0)) {
canvas.arc(canvas.x2px(keys[0]) + 4, canvas.y2px(0), 4, Math.PI / 2, 3 * Math.PI / 2)
}
}
for(let i = 0; i < keys.length - 1; i++) {
let idx = keys[i]
currentY += parseFloat(this.probabilities[idx].replace(/,/g, "."))
if(canvas.isVisible(idx, currentY) || canvas.isVisible(keys[i + 1], currentY)) {
canvas.drawLine(
Math.max(0, canvas.x2px(idx)),
canvas.y2px(currentY),
Math.min(canvas.width, canvas.x2px(keys[i + 1])),
canvas.y2px(currentY)
)
if(canvas.isVisible(idx, currentY)) {
canvas.disc(canvas.x2px(idx), canvas.y2px(currentY), 4)
}
if(canvas.isVisible(keys[i + 1], currentY)) {
canvas.arc(canvas.x2px(keys[i + 1]) + 4, canvas.y2px(currentY), 4, Math.PI / 2, 3 * Math.PI / 2)
}
}
}
if(canvas.isVisible(keys[keys.length - 1], currentY + parseFloat(this.probabilities[keys[keys.length - 1]]))) {
canvas.drawLine(
Math.max(0, canvas.x2px(keys[keys.length - 1])),
canvas.y2px(currentY + parseFloat(this.probabilities[keys[keys.length - 1]].replace(/,/g, "."))),
canvas.width,
canvas.y2px(currentY + parseFloat(this.probabilities[keys[keys.length - 1]].replace(/,/g, ".")))
)
canvas.disc(
canvas.x2px(keys[keys.length - 1]),
canvas.y2px(
currentY + parseFloat(this.probabilities[keys[keys.length - 1]].replace(/,/g, "."))
),
4
)
}
// Label
this.drawLabel(canvas, this.labelPosition, canvas.x2px(this.labelX), canvas.y2px(this.execute(this.labelX)))
}
}

View file

@ -0,0 +1,201 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 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/>.
*/
import { textsub } from "../utils.mjs"
import Objects from "../module/objects.mjs"
import { ExecutableObject } from "./common.mjs"
import { parseDomain, Expression, SpecialDomain } from "../math/index.mjs"
import * as P from "../parameters.mjs"
import Latex from "../module/latex.mjs"
export default class Function extends ExecutableObject {
static type() {
return "Function"
}
static displayType() {
return qsTranslate("function", "Function")
}
static displayTypeMultiple() {
return qsTranslate("function", "Functions")
}
static properties() {
return {
[QT_TRANSLATE_NOOP("prop", "expression")]: new P.Expression("x"),
[QT_TRANSLATE_NOOP("prop", "definitionDomain")]: "Domain",
[QT_TRANSLATE_NOOP("prop", "destinationDomain")]: "Domain",
"comment1": QT_TRANSLATE_NOOP(
"comment",
"Ex: R+* (ℝ⁺*), N (), Z-* (ℤ⁻*), ]0;1[, {3;4;5}"
),
[QT_TRANSLATE_NOOP("prop", "displayMode")]: P.Enum.FunctionDisplayType,
[QT_TRANSLATE_NOOP("prop", "labelPosition")]: P.Enum.Position,
[QT_TRANSLATE_NOOP("prop", "labelX")]: "number",
"comment2": QT_TRANSLATE_NOOP(
"comment",
"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"
}
}
constructor(name = null, visible = true, color = null, labelContent = "name + value",
expression = "x", definitionDomain = "RPE", destinationDomain = "R",
displayMode = "application", labelPosition = "above", labelX = 1,
drawPoints = true, drawDashedLines = true) {
if(name == null) name = Objects.getNewName("fghjqlmnopqrstuvwabcde")
super(name, visible, color, labelContent)
if(typeof expression == "number" || typeof expression == "string") expression = new Expression(expression.toString())
this.expression = expression
if(typeof definitionDomain == "string") definitionDomain = parseDomain(definitionDomain)
this.definitionDomain = definitionDomain
if(typeof destinationDomain == "string") destinationDomain = parseDomain(destinationDomain)
this.destinationDomain = destinationDomain
this.displayMode = displayMode
this.labelPosition = labelPosition
this.labelX = labelX
this.drawPoints = drawPoints
this.drawDashedLines = drawDashedLines
}
getReadableString() {
if(this.displayMode === "application") {
return `${this.name}: ${this.definitionDomain}${this.destinationDomain}\n ${" ".repeat(this.name.length)}x ⟼ ${this.expression.toString()}`
} else {
return `${this.name}(x) = ${this.expression.toString()}\nD${textsub(this.name)} = ${this.definitionDomain}`
}
}
getLatexString() {
if(this.displayMode === "application") {
return `${Latex.variable(this.name)}:\\begin{array}{llll}${this.definitionDomain.latexMarkup}\\textrm{ } & \\rightarrow & \\textrm{ }${this.destinationDomain.latexMarkup}\\\\
x\\textrm{ } & \\mapsto & \\textrm{ }${this.expression.latexMarkup}\\end{array}`
} else {
return `\\begin{array}{l}${Latex.variable(this.name)}(x) = ${this.expression.latexMarkup}\\\\ D_{${this.name}} = ${this.definitionDomain.latexMarkup}\\end{array}`
}
}
execute(x = 1) {
if(this.definitionDomain.includes(x))
return this.expression.execute(x)
return null
}
canExecute(x = 1) {
return this.definitionDomain.includes(x)
}
simplify(x = 1) {
if(this.definitionDomain.includes(x))
return this.expression.simplify(x)
return ""
}
draw(canvas) {
Function.drawFunction(canvas, this.expression, this.definitionDomain, this.destinationDomain, this.drawPoints, this.drawDashedLines)
// Label
this.drawLabel(canvas, this.labelPosition, canvas.x2px(this.labelX), canvas.y2px(this.execute(this.labelX)))
}
/**
* Reusable in other objects.
* Drawing small traits every few pixels
*/
static drawFunction(canvas, expr, definitionDomain, destinationDomain, drawPoints = true, drawDash = true) {
let pxprecision = 10
let previousX = canvas.px2x(0)
let previousY = null
if(definitionDomain instanceof SpecialDomain && definitionDomain.moveSupported) {
// Point based functions.
previousX = definitionDomain.next(previousX)
if(previousX === null) previousX = definitionDomain.next(canvas.px2x(0))
previousY = expr.execute(previousX)
if(!drawPoints && !drawDash) return
while(previousX !== null && canvas.x2px(previousX) < canvas.width) {
// Reconverted for canvas to fix for logarithmic scales.
let currentX = definitionDomain.next(canvas.px2x(canvas.x2px(previousX) + pxprecision))
let currentY = expr.execute(currentX)
if(currentX === null) break
if((definitionDomain.includes(currentX) || definitionDomain.includes(previousX)) &&
(destinationDomain.includes(currentY) || destinationDomain.includes(previousY))) {
if(drawDash)
canvas.drawDashedLine(canvas.x2px(previousX), canvas.y2px(previousY), canvas.x2px(currentX), canvas.y2px(currentY))
if(drawPoints) {
canvas.fillRect(canvas.x2px(previousX) - 5, canvas.y2px(previousY) - 1, 10, 2)
canvas.fillRect(canvas.x2px(previousX) - 1, canvas.y2px(previousY) - 5, 2, 10)
}
}
previousX = currentX
previousY = currentY
}
if(drawPoints) {
// Drawing the last cross
canvas.fillRect(canvas.x2px(previousX) - 5, canvas.y2px(previousY) - 1, 10, 2)
canvas.fillRect(canvas.x2px(previousX) - 1, canvas.y2px(previousY) - 5, 2, 10)
}
} else {
// Use max precision if function is trigonometrical on log scale.
let exprString = expr.expr
if(exprString.includes("sin") || exprString.includes("cos") || exprString.includes("tan"))
pxprecision = (canvas.logscalex || exprString.includes("tan")) ? 1 : 3
// Calculate the previousY at the start of the canvas
if(definitionDomain.includes(previousX))
previousY = expr.execute(previousX)
for(let px = pxprecision; px < canvas.width; px += pxprecision) {
let currentX = canvas.px2x(px)
if(!definitionDomain.includes(previousX) && definitionDomain.includes(currentX)) {
// Should draw up to currentX, but NOT at previousX.
// Need to find the starting point.
let tmpPx = px - pxprecision
do {
tmpPx++
previousX = canvas.px2x(tmpPx)
} while(!definitionDomain.includes(previousX))
// Recaclulate previousY
previousY = expr.execute(previousX)
} else if(!definitionDomain.includes(currentX)) {
// Next x is NOT in the definition domain.
// Augmenting the pixel precision until this condition is fulfilled.
let tmpPx = px
do {
tmpPx--
currentX = canvas.px2x(tmpPx)
} while(!definitionDomain.includes(currentX) && currentX !== previousX)
}
// This max variation is needed for functions with asymptotical vertical lines (e.g. 1/x, tan x...)
let maxvariation = (canvas.px2y(0) - canvas.px2y(canvas.height))
if(definitionDomain.includes(previousX) && definitionDomain.includes(currentX)) {
let currentY = expr.execute(currentX)
if(destinationDomain.includes(currentY)) {
if(previousY != null && destinationDomain.includes(previousY) && Math.abs(previousY - currentY) < maxvariation) {
canvas.drawLine(canvas.x2px(previousX), canvas.y2px(previousY), canvas.x2px(currentX), canvas.y2px(currentY))
}
}
previousY = currentY
} else {
previousY = null // Last y was invalid, so let's not draw anything from it.
}
previousX = canvas.px2x(px)
}
}
}
}

87
common/src/objs/point.mjs Normal file
View file

@ -0,0 +1,87 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 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/>.
*/
import { Expression } from "../math/index.mjs"
import * as P from "../parameters.mjs"
import Objects from "../module/objects.mjs"
import Latex from "../module/latex.mjs"
import { DrawableObject } from "./common.mjs"
export default class Point extends DrawableObject {
static type() {
return "Point"
}
static displayType() {
return qsTranslate("point", "Point")
}
static displayTypeMultiple() {
return qsTranslate("point", "Points")
}
static properties() {
return {
[QT_TRANSLATE_NOOP("prop", "x")]: new P.Expression(),
[QT_TRANSLATE_NOOP("prop", "y")]: new P.Expression(),
[QT_TRANSLATE_NOOP("prop", "labelPosition")]: P.Enum.Position,
[QT_TRANSLATE_NOOP("prop", "pointStyle")]: new P.Enum("●", "✕", "")
}
}
constructor(name = null, visible = true, color = null, labelContent = "name + value",
x = 1, y = 0, labelPosition = "above", pointStyle = "●") {
if(name == null) name = Objects.getNewName("ABCDEFJKLMNOPQRSTUVW")
super(name, visible, color, labelContent)
if(typeof x == "number" || typeof x == "string") x = new Expression(x.toString())
this.x = x
if(typeof y == "number" || typeof y == "string") y = new Expression(y.toString())
this.y = y
this.labelPosition = labelPosition
this.pointStyle = pointStyle
}
getReadableString() {
return `${this.name} = (${this.x}, ${this.y})`
}
getLatexString() {
return `${Latex.variable(this.name)} = \\left(${this.x.latexMarkup}, ${this.y.latexMarkup}\\right)`
}
draw(canvas) {
let [canvasX, canvasY] = [canvas.x2px(this.x.execute()), canvas.y2px(this.y.execute())]
let pointSize = 8 + (canvas.linewidth * 2)
switch(this.pointStyle) {
case "●":
canvas.disc(canvasX, canvasY, pointSize / 2)
break
case "✕":
canvas.drawLine(canvasX - pointSize / 2, canvasY - pointSize / 2, canvasX + pointSize / 2, canvasY + pointSize / 2)
canvas.drawLine(canvasX - pointSize / 2, canvasY + pointSize / 2, canvasX + pointSize / 2, canvasY - pointSize / 2)
break
case "":
canvas.fillRect(canvasX - pointSize / 2, canvasY - 1, pointSize, 2)
canvas.fillRect(canvasX - 1, canvasY - pointSize / 2, 2, pointSize)
break
}
this.drawLabel(canvas, this.labelPosition, canvasX, canvasY)
}
}

View file

@ -0,0 +1,140 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 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/>.
*/
import { Sequence as MathSequence, Domain } from "../math/index.mjs"
import * as P from "../parameters.mjs"
import Latex from "../module/latex.mjs"
import Objects from "../module/objects.mjs"
import { ExecutableObject } from "./common.mjs"
import Function from "./function.mjs"
export default class Sequence extends ExecutableObject {
static type() {
return "Sequence"
}
static displayType() {
return qsTranslate("sequence", "Sequence")
}
static displayTypeMultiple() {
return qsTranslate("sequence", "Sequences")
}
static properties() {
return {
[QT_TRANSLATE_NOOP("prop", "drawPoints")]: "boolean",
[QT_TRANSLATE_NOOP("prop", "drawDashedLines")]: "boolean",
[QT_TRANSLATE_NOOP("prop", "defaultExpression")]: new P.Dictionary("string", "int", /^.+$/, /^\d+$/, "{name}[n+", "] = ", true),
"comment1": QT_TRANSLATE_NOOP(
"comment",
"Note: Use %1[n] to refer to %1ₙ, %1[n+1] for %1ₙ₊₁..."
),
[QT_TRANSLATE_NOOP("prop", "baseValues")]: new P.Dictionary("string", "int", /^.+$/, /^\d+$/, "{name}[", "] = "),
[QT_TRANSLATE_NOOP("prop", "labelPosition")]: P.Enum.Position,
[QT_TRANSLATE_NOOP("prop", "labelX")]: "number"
}
}
constructor(name = null, visible = true, color = null, labelContent = "name + value",
drawPoints = true, drawDashedLines = true, defaultExp = { 1: "n" },
baseValues = { 0: 0 }, labelPosition = "above", labelX = 1) {
if(name == null) name = Objects.getNewName("uvwPSUVWabcde")
super(name, visible, color, labelContent)
this.drawPoints = drawPoints
this.drawDashedLines = drawDashedLines
this.defaultExpression = defaultExp
this.baseValues = baseValues
this.labelPosition = labelPosition
this.labelX = labelX
this.update()
}
update() {
console.log("Updating sequence", this.sequence)
super.update()
if(
this.sequence == null || this.baseValues !== this.sequence.baseValues ||
this.sequence.name !== this.name ||
this.sequence.expr !== Object.values(this.defaultExpression)[0] ||
this.sequence.valuePlus.toString() !== Object.keys(this.defaultExpression)[0]
)
this.sequence = new MathSequence(
this.name, this.baseValues,
parseFloat(Object.keys(this.defaultExpression)[0]),
Object.values(this.defaultExpression)[0]
)
}
getReadableString() {
return this.sequence.toString()
}
getLatexString() {
return this.sequence.toLatexString()
}
execute(x = 1) {
if(x % 1 === 0)
return this.sequence.execute(x)
return null
}
canExecute(x = 1) {
return x % 1 === 0
}
// Simplify returns the simplified string of the expression.
simplify(x = 1) {
if(x % 1 === 0)
return this.sequence.simplify(x)
return null
}
getLabel() {
switch(this.labelContent) {
case "name":
return `(${this.name}ₙ)`
case "name + value":
return this.getReadableString()
case "null":
return ""
}
}
getLatexLabel() {
switch(this.labelContent) {
case "name":
return `(${Latex.variable(this.name)}_n)`
case "name + value":
return this.getLatexString()
case "null":
return ""
}
}
draw(canvas) {
Function.drawFunction(canvas, this.sequence, canvas.logscalex ? Domain.NE : Domain.N, Domain.R, this.drawPoints, this.drawDashedLines)
// Label
this.drawLabel(canvas, this.labelPosition, canvas.x2px(this.labelX), canvas.y2px(this.execute(this.labelX)))
}
}

108
common/src/objs/text.mjs Normal file
View file

@ -0,0 +1,108 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 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/>.
*/
import { Expression } from "../math/index.mjs"
import * as P from "../parameters.mjs"
import Objects from "../module/objects.mjs"
import Latex from "../module/latex.mjs"
import { DrawableObject } from "./common.mjs"
export default class Text extends DrawableObject {
static type() {
return "Text"
}
static displayType() {
return qsTranslate("text", "Text")
}
static displayTypeMultiple() {
return qsTranslate("text", "Texts")
}
static properties() {
return {
[QT_TRANSLATE_NOOP("prop", "x")]: new P.Expression(),
[QT_TRANSLATE_NOOP("prop", "y")]: new P.Expression(),
[QT_TRANSLATE_NOOP("prop", "labelPosition")]: P.Enum.Positioning,
[QT_TRANSLATE_NOOP("prop", "text")]: "string",
"comment1": QT_TRANSLATE_NOOP(
"comment",
"If you have latex enabled, you can use use latex markup in between $$ to create equations."
),
[QT_TRANSLATE_NOOP("prop", "disableLatex")]: "boolean"
}
}
constructor(name = null, visible = true, color = null, labelContent = "null",
x = 1, y = 0, labelPosition = "center", text = "New text", disableLatex = false) {
if(name == null) name = Objects.getNewName("t")
super(name, visible, color, labelContent)
if(typeof x == "number" || typeof x == "string") x = new Expression(x.toString())
this.x = x
if(typeof y == "number" || typeof y == "string") y = new Expression(y.toString())
this.y = y
this.labelPosition = labelPosition
this.text = text
this.disableLatex = disableLatex
}
getReadableString() {
return `${this.name} = "${this.text}"`
}
latexMarkupText() {
// Check whether the text contains latex escaped elements.
let txt = []
this.text.split("$$").forEach(function(t) {
txt = txt.concat(Latex.variable(t, true).replace(/\$\$/g, "").split("$"))
})
let newTxt = txt[0]
let i
// Split between normal text and latex escaped.
for(i = 0; i < txt.length - 1; i++)
if(i & 0x01) // Every odd number
newTxt += "\\textsf{" + Latex.variable(txt[i + 1])
else
newTxt += "}" + txt[i + 1]
// Finished by a }
if(i & 0x01)
newTxt += "{"
return newTxt
}
getLatexString() {
return `${Latex.variable(this.name)} = "\\textsf{${this.latexMarkupText()}}"`
}
getLabel() {
return this.text
}
getLatexLabel() {
return `\\textsf{${this.latexMarkupText()}}`
}
draw(canvas) {
let yOffset = this.disableLatex ? canvas.textsize - 4 : 0
this.drawLabel(canvas, this.labelPosition, canvas.x2px(this.x.execute()), canvas.y2px(this.y.execute()) + yOffset, this.disableLatex)
}
}

167
common/src/objs/xcursor.mjs Normal file
View file

@ -0,0 +1,167 @@
/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 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/>.
*/
import { Expression } from "../math/index.mjs"
import * as P from "../parameters.mjs"
import Latex from "../module/latex.mjs"
import Objects from "../module/objects.mjs"
import { DrawableObject } from "./common.mjs"
export default class XCursor extends DrawableObject {
static type(){return 'X Cursor'}
static displayType(){return qsTranslate("xcursor", 'X Cursor')}
static displayTypeMultiple(){return qsTranslate("xcursor", 'X Cursors')}
static properties() {return {
[QT_TRANSLATE_NOOP('prop','x')]: new P.Expression(),
[QT_TRANSLATE_NOOP('prop','targetElement')]: new P.ObjectType('ExecutableObject', true),
[QT_TRANSLATE_NOOP('prop','labelPosition')]: P.Enum.Position,
[QT_TRANSLATE_NOOP('prop','approximate')]: 'boolean',
[QT_TRANSLATE_NOOP('prop','rounding')]: 'int',
[QT_TRANSLATE_NOOP('prop','displayStyle')]: new P.Enum(
'— — — — — — —',
'⸺⸺⸺⸺⸺⸺',
'• • • • • • • • • •'
),
[QT_TRANSLATE_NOOP('prop','targetValuePosition')]: P.Enum.XCursorValuePosition,
}}
constructor(name = null, visible = true, color = null, labelContent = 'name + value',
x = 1, targetElement = null, labelPosition = 'left', approximate = true,
rounding = 3, displayStyle = '— — — — — — —', targetValuePosition = 'Next to target') {
if(name == null) name = Objects.getNewName('X')
super(name, visible, color, labelContent)
this.approximate = approximate
this.rounding = rounding
if(typeof x == 'number' || typeof x == 'string') x = new Expression(x.toString())
this.x = x
this.targetElement = targetElement
if(typeof targetElement == "string") {
this.targetElement = Objects.currentObjectsByName[targetElement]
}
this.labelPosition = labelPosition
this.displayStyle = displayStyle
this.targetValuePosition = targetValuePosition
}
getReadableString() {
if(this.targetElement == null) return `${this.name} = ${this.x.toString()}`
return `${this.name} = ${this.x.toString()}\n${this.getTargetValueLabel()}`
}
getLatexString() {
if(this.targetElement == null) return `${Latex.variable(this.name)} = ${this.x.latexMarkup}`
return `\\begin{array}{l}
${Latex.variable(this.name)} = ${this.x.latexMarkup} \\\\
${this.getTargetValueLatexLabel()}
\\end{array}`
}
getApprox() {
let approx = ''
if(this.approximate) {
approx = (this.targetElement.execute(this.x.execute()))
let intLength = Math.round(approx).toString().length
let rounding = Math.min(this.rounding, approx.toString().length - intLength - 1)
approx = approx.toPrecision(rounding + intLength)
}
return approx
}
getTargetValueLabel() {
const t = this.targetElement
const approx = this.getApprox()
return `${t.name}(${this.name}) = ${t.simplify(this.x.toEditableString())}` +
(this.approximate ? ' ≈ ' + approx : '')
}
getTargetValueLatexLabel() {
const t = this.targetElement
const approx = this.getApprox()
const simpl = t.simplify(this.x.toEditableString())
return `${Latex.variable(t.name)}(${Latex.variable(this.name)}) = ${simpl.latexMarkup ? simpl.latexMarkup : Latex.variable(simpl)}` +
(this.approximate ? ' \\simeq ' + approx : '')
}
getLabel() {
switch(this.labelContent) {
case 'name':
return this.name
case 'name + value':
switch(this.targetValuePosition) {
case 'Next to target':
case 'Hidden':
return `${this.name} = ${this.x.toString()}`
case 'With label':
return this.getReadableString()
}
case 'null':
return ''
}
}
getLatexLabel() {
switch(this.labelContent) {
case 'name':
return Latex.variable(this.name)
case 'name + value':
switch(this.targetValuePosition) {
case 'Next to target':
case 'Hidden':
return `${Latex.variable(this.name)} = ${this.x.latexMarkup}`
case 'With label':
return this.getLatexString()
}
case 'null':
return ''
}
}
draw(canvas) {
let xpos = canvas.x2px(this.x.execute())
switch(this.displayStyle) {
case '— — — — — — —':
canvas.drawDashedLine(xpos, 0, xpos, canvas.height, 20)
break;
case '⸺⸺⸺⸺⸺⸺':
canvas.drawXLine(this.x.execute())
break;
case '• • • • • • • • • •':
let pointDistancePx = 10
let pointSize = 2
for(let i = 0; i < canvas.height; i += pointDistancePx)
canvas.disc(xpos, i, pointSize)
break;
}
// Drawing label at the top of the canvas.
this.drawLabel(canvas, this.labelPosition, xpos, 0, false, null, null,
(x,y,ltxImg) => canvas.drawVisibleImage(ltxImg.source, x, 5, ltxImg.width, ltxImg.height),
(x,y,text,textSize) => canvas.drawVisibleText(text, x, textSize.height+5))
// Drawing label at the position of the target element.
if(this.targetValuePosition === 'Next to target' && this.targetElement != null) {
let ypos = canvas.y2px(this.targetElement.execute(this.x.execute()))
this.drawLabel(canvas, this.labelPosition, xpos, ypos, false,
this.getTargetValueLatexLabel.bind(this), this.getTargetValueLabel.bind(this),
(x,y,ltxImg) => canvas.drawVisibleImage(ltxImg.source, x, y, ltxImg.width, ltxImg.height),
(x,y,text,textSize) => canvas.drawVisibleText(text, x, y+textSize.height))
}
}
}