modules/aleaFigure/GraphicView.js

/* eslint-disable no-unused-vars */
import { randomInt } from 'mathjs'
import { GVCartesian } from './coordinates.js'
import { GVRectangle, GVTriangle, GVPolygon, GVVector, GVAngle, GVPoint, GVLine, GVSegment, GVGraphicObject, GVCircle, barycentre } from './elements.js'
import { getMathalea2DExport } from './getMathalea2DExport.js'
import { circularPermutation, quotient } from './outils.js'
import { aleaName } from '../outilsMathjs.js'
/**
 * Donne une liste d'entiers relatifs dont on connait la somme.
 * @example > listeEntiersSommeConnue(4,10,-2)
 * < [1,-2,6,5]
 * @param {int} nbElements Nombre d'éléments de la liste
 * @param {int} total Somme des éléments de la liste (peut être un nombre négatif)
 * @param {int} [valMin = 1] Valeur minimale de chaque élément (peut être un nombre négatif)
 * @return {Array} Liste d'entiers relatifs
 * @author Frédéric PIOU
 */
export function listeEntiersSommeConnue (nbElements, total, valMin = 1) {
  const liste = []
  liste.push(randomInt(valMin, total - (nbElements - 1) * valMin + 1))
  for (let j = 1; j < nbElements - 1; j++) {
    liste.push(randomInt(liste[j - 1] + valMin, total - (nbElements - j - 1) * valMin + 1))
  }
  liste.push(total)
  for (let j = nbElements - 1; j > 0; j--) {
    liste[j] = liste[j] - liste[j - 1]
  }
  return liste
}
/**
  * @class
  * @classdesc Caracteristics of a graphic view
  * @author Frédéric PIOU
  */
export class GVGraphicView {
  names/** string[] */
  ppc /** number */
  scale /** number */
  geometric /**  GVGraphicObject[] */
  geometricExport /**  GVGraphicObject[] */
  grid /** GVLine[] */
  axes /** GVLine[] */
  xmin /** number */ = 0
  ymin /** number */ = 0
  xmax /** number */ = 10
  ymax /** number */ = 10
  width /** number */
  height /** number */
  ratio /** number */
  clipVisible /** boolean */ = false
  saveRatio /** boolean */ = true
  allowResize /** boolean */ = true
  showLabelPoint /** boolean */ = false
  limit /** number */ = Infinity
  _namesAlea /** boolean */ = true
  constructor (xmin /** number */ = 0, ymin /** number */ = 0, xmax /** number */ = 10, ymax /** number */ = 10) {
    this.setDimensions(xmin, ymin, xmax, ymax)
    this.names = aleaName('ABCDEFGHIJKLMNOPRSTUVZ'.split(''))
    this.ppc = 20 // Pixels per Centimeter
    this.scale = 1 // Scale for Tikz
    this.geometric = []
    this.grid = []
    this.axes = []
  }

  set namesAlea (names /** string[] | boolean) */) {
    if (typeof names === 'string') this.names = aleaName(names)
    if (names === false) this.names = 'ABCDEFGHIJKLMNOPRSTUVZ'.split('')
  }

  get namesAlea () {
    return this.names
  }

  setDimensions (xmin = -5, ymin = -5, xmax = 5, ymax = 5) {
    this.xmin = xmin
    this.ymin = ymin
    this.xmax = xmax
    this.ymax = ymax
    this.width = this.xmax - this.xmin
    this.height = this.ymax - this.ymin
    this.ratio = this.height / this.width
  }

  /**
    * Show any Objects in Mathalea2D
    * @example
    * show(A,B,C,ABC)
    */
  show (...args /**  GVGraphicObject[] */) /**  GVGraphicObject[] */ {
    const group = []
    args.forEach(x => {
      if (Array.isArray(x)) {
        group.push(...x)
      } else if (x instanceof GVPolygon) {
        group.push(...x.vertices)
        group.push(...this.addSidesPolygon(...x.vertices))
        this.addLabelsPointsPolygon(...x.vertices)
      } else {
        group.push(x)
      }
    })
    group.forEach(x => { x.visible = true })
    return group
  }

  addGrid (step /** number */ = 1) {
    for (let i = this.xmin; i < quotient(this.width, step); i++) {
      const lineX = new GVLine(new GVPoint(i, 0), new GVVector(0, 1))
      lineX.color = 'gray'
      this.grid.push(lineX)
    }
    for (let i = this.ymin; i < quotient(this.height, step); i++) {
      const lineY = new GVLine(new GVPoint(0, i), new GVVector(1, 0))
      lineY.color = 'gray'
      this.grid.push(lineY)
    }
  }

  addAxes () {
    const lineX = new GVLine(new GVPoint(0, 0), new GVVector(0, 1))
    this.axes.push(lineX)
    const lineY = new GVLine(new GVPoint(0, 0), new GVVector(1, 0))
    this.axes.push(lineY)
  }

  getDimensions (...liste /**  GVGraphicObject[] */)/** [number, number, number, number] */ {
    const listPoint = this.getListObjectTypeSelect('Point', liste)
    const listXPoint = listPoint.map((X/** { x } */) => { return X.x })
    const listYPoint = listPoint.map((Y/** { y } */) => { return Y.y })
    const xmin = Math.min(...listXPoint)
    const xmax = Math.max(...listXPoint)
    const ymin = Math.min(...listYPoint)
    const ymax = Math.max(...listYPoint)
    return [xmin, ymin, xmax, ymax]
  }

  getWidth (...liste /**  GVGraphicObject[] */) /** number */ {
    const [xmin, ymin, xmax, ymax] = this.getDimensions(...liste)
    return xmax - xmin
  }

  getHeight (...liste /**  GVGraphicObject[] */) /** number */ {
    const [xmin, ymin, xmax, ymax] = this.getDimensions(...liste)
    return ymax - ymin
  }

  getUponPoint (...liste /**  GVGraphicObject[] */) /** GVPoint */ {
    const listePoints = this.getListObjectTypeSelect('Point', liste)
    return this.getListObjectTypeSelect('Point', liste).sort((a, b) => b.y - a.y)[listePoints.length - 1]
  }

  geBelowPoint (...liste /**  GVGraphicObject[] */) /** GVPoint */ {
    const listePoints = this.getListObjectTypeSelect('Point', liste)
    return this.getListObjectTypeSelect('Point', liste).sort((a, b) => b.y - a.y)[0]
  }

  getLeftPoint (...liste /**  GVGraphicObject[] */) /** GVPoint */ {
    const listePoints = this.getListObjectTypeSelect('Point', liste)
    return this.getListObjectTypeSelect('Point', liste).sort((a, b) => b.x - a.x)[listePoints.length - 1]
  }

  getRightPoint (...liste /**  GVGraphicObject[] */) /** GVPoint */ {
    const listePoints = this.getListObjectTypeSelect('Point', liste)
    return listePoints.sort((a, b) => b.x - a.x)[0]
  }

  /**
    * Resize window of graphic view to the created points
    * Keep the ratio
    */
  resize () {
    // On commence par déterminer les dimensions à partir des abscisses et des ordonnées extrèmes
    const [xmin, ymin, xmax, ymax] = this.getDimensions()
    const [width, height] = [xmax - xmin, ymax - ymin]
    // On fait une copie des dimensions obtenues
    let newheight = 0 + height
    let newwidth = 0 + width
    // On calcule le ratio obtenu
    const ratio = height / width
    const k = ratio / this.ratio
    // S'il faut conserver le ratio on ne modifie qu'une seule dimension
    if (this.saveRatio) {
      if (k < 1) { // La largeur est plus grande
        newheight = height / k
      } else { // La hauteur est plus grande
        newwidth = width * k
      }
    } else { // Sinon seul le ratio change
      this.ratio = newheight / newwidth
    }
    // La hauteur sera utilisée dans tous les cas pour le calcul de l'échelle.
    // Elle sera donc conservée si le ratio ne l'est pas.
    this.ppc = this.ppc * this.height / newheight
    this.scale = this.scale * this.height / newheight
    const deltaX = (newwidth - width) / 2
    const deltaY = (newheight - height) / 2
    this.setDimensions(xmin - deltaX, ymin - deltaY, xmax + deltaX, ymax + deltaY)
  }

  /**
    * Give the list sorted of object with a given type
    */
  getListObjectTypeSelect (typeSelect = 'Point', liste /**  GVGraphicObject[] */ = this.geometric)/** any */ {
    if (liste.length === 0) liste = this.geometric
    switch (typeSelect) {
      case 'Point':
        return liste.filter(obj => obj instanceof GVPoint).sort(
          (a, b) => {
            const nameA = a.name.split('_')
            const nameB = b.name.split('_')
            if (nameA[0] === nameB[0]) {
              return parseInt(nameA[1]) - parseInt(nameB[1])
            } else {
              return (-1) ** (nameA[0] > nameB[0] ? 0 : 1)
            }
          }
        )
      default:
        return this.geometric.filter(obj => !(obj instanceof GVPoint)).sort(
          (a, b) => {
            const nameA = a.name.split('_')
            const nameB = b.name.split('_')
            if (nameA[0] === nameB[0]) {
              return parseInt(nameA[1]) - parseInt(nameB[1])
            } else {
              return (-1) ** (nameA[0] > nameB[0] ? 0 : 1)
            }
          }
        )
    }
  }

  /**
    * Search the last name not used and give a new name
    */
  getLastNameNotUsed (typeSelect = 'Point') {
    switch (typeSelect) {
      case 'Point': {
        const list = this.getListObjectTypeSelect('Point')
        let nonUseLetter
        let i = 0
        do {
          const indice = i === 0 ? '' : `_${i}`
          nonUseLetter = this.names.find(
            letter => list.filter(x => x.name[0] === letter).find(
              obj => obj.name === letter + indice
            )?.name !== letter + indice)
          if (nonUseLetter !== undefined) nonUseLetter += indice
          i += 1
        } while (nonUseLetter === undefined)
        return nonUseLetter
      }
      case 'Segment':
      case 'Circle':
      case 'Line': {
        const list = this.getListObjectTypeSelect('Segment').concat(this.getListObjectTypeSelect('Line'))
        let nonUseLetter
        let i = 0
        do {
          const indice = i === 0 ? '' : `_${i}`
          nonUseLetter = this.names.find(
            letter => list.filter(x => x.name[0] === letter.toLowerCase()).find(
              obj => obj.name === letter.toLowerCase() + indice
            )?.name !== letter.toLowerCase() + indice)
          if (nonUseLetter !== undefined) nonUseLetter = nonUseLetter.toLowerCase() + indice
          i += 1
        } while (nonUseLetter === undefined)
        return nonUseLetter
        // return this.names.find(letter => list.find(obj => obj.name.split('')[0] === letter.toLowerCase())?.name.split('_')[0] !== letter.toLowerCase()).toLowerCase()
      }
    }
  }

  /**
    * Give a new name
    */
  getNewName (typeSelect = 'Point') {
    switch (typeSelect) {
      case 'Point':
        return this.getLastNameNotUsed(typeSelect)
      case 'Segment':
      case 'Circle':
      case 'Line':
        return this.getLastNameNotUsed(typeSelect)
    }
  }

  aleaNameObject (...args /**  GVGraphicObject[] */) {
    const names = aleaName(args.length)
    args.forEach((x, i) => { x.name = names[i] })
  }

  /**
    * Append new objects to the euclidean plan
    * @param {number} n Number of point to create
    * @param {number} step For coordinates
    * @example
    * this.addPoint(2,0.5) --> Add two points with coordinates step 0.5 precision
    */
  addPoint (n /** number */ = 1, step /** number */) /** GVPoint[] */ {
    // Il faudrait donner la possibilité d'ajouter des points définis par leurs coordonnées
    const newPoints = []
    for (let i = 0; i < n; i++) {
      let obj /** GVPoint */
      let cpt = 0
      do {
        cpt += 1
        obj = new GVPoint(
          new GVCartesian(
            step === undefined ? Math.random() * this.width + this.xmin : quotient(Math.random() * this.width + this.xmin, step),
            step === undefined ? Math.random() * this.height + this.ymin : quotient(Math.random() * this.height + this.ymin, step)
          )
        )
      } while (cpt < this.limit && (this.isCloseToExistingPoints(obj) || this.isCloseToLineThroughtExistingPoints(obj)))
      obj.name = this.getNewName(obj.type)
      if (this.showLabelPoint) {
        obj.showDot()
        obj.showName()
      }
      this.geometric.push(obj)
      newPoints.push(obj)
    }
    return newPoints
  }

  /**
    * Add intersect point of two lines in the view
    */
  addIntersectLine (line1/** GVLine | GVCircle */, line2/** GVLine | GVCircle */) {
    if (line1 instanceof GVLine && line2 instanceof GVLine) {
      const delta = line1.a * line2.b - line2.a * line1.b
      if (delta.toFixed(15) !== '0') {
        const deltax = -(line1.b * line2.c - line2.b * line1.c)
        const deltay = line1.a * line2.c - line2.a * line1.c
        const point = new GVPoint(new GVCartesian(deltax / delta, deltay / delta))
        point.name = this.getNewName(point.type)
        this.geometric.push(point)
        return [point]
      }
    } else if (line1 instanceof GVCircle && line2 instanceof GVCircle) {
      const d = this.distance(line1.A, line2.A)
      if (d > line1.r + line2.r || d < (Math.abs(line1.r - line2.r))) {
        return []
      } else {
        const a = (line1.r ** 2 - line2.r ** 2 + d ** 2) / (2 * d)
        const h = Math.sqrt(line1.r ** 2 - a ** 2)
        const x2 = line1.A.x + a * (line2.A.x - line1.A.x) / d
        const y2 = line1.A.y + a * (line2.A.y - line1.A.y) / d
        const x3 = x2 + h * (line2.A.y - line1.A.y) / d
        const y3 = y2 - h * (line2.A.x - line1.A.x) / d
        const P1 = new GVPoint(new GVCartesian(x3, y3))
        const x4 = x2 - h * (line2.A.y - line1.A.y) / d
        const y4 = y2 + h * (line2.A.x - line1.A.x) / d
        const P2 = new GVPoint(new GVCartesian(x4, y4))
        P1.name = P1.name || this.getNewName(P1.type)
        P2.name = P2.name || this.getNewName(P2.type)
        this.geometric.push(P1, P2)
        return [P1, P2]
      }
    }
  }

  /**
    * Zoom in or out
    */
  zoom (k /** number */ = 1.01) {
    const xmin = k * (this.xmin - (this.xmax + this.xmin) / 2) + (this.xmax + this.xmin) / 2
    const xmax = k * (this.xmax - (this.xmax + this.xmin) / 2) + (this.xmax + this.xmin) / 2
    const ymin = k * (this.ymin - (this.ymax + this.ymin) / 2) + (this.ymax + this.ymin) / 2
    const ymax = k * (this.ymax - (this.ymax + this.ymin) / 2) + (this.ymax + this.ymin) / 2
    this.setDimensions(xmin, ymin, xmax, ymax)
  }

  /**
    * Give the distance between tow points, a point and a line, two lines
    */
  distance (P /** GVPoint */, Y /** GVPoint | GVLine */) {
    if (Y instanceof GVPoint) {
      return Math.sqrt((P.x - Y.x) ** 2 + (P.y - Y.y) ** 2)
    } else {
      return Math.abs(Y.a * P.x + Y.b * P.y - Y.c) / Math.sqrt(Y.a ** 2 + Y.b ** 2)
    }
  }

  /**
    * Tempt to estimate if a point is close to the existing points
    */
  isCloseToExistingPoints (M /** GVPoint */) {
    const listExistingPoints = this.getListObjectTypeSelect('Point')
    const maxDistance = Math.min(this.height, this.width) / listExistingPoints.length / 3
    if (listExistingPoints.length > 0) { return listExistingPoints.some(X => this.distance(X, M) < maxDistance) }
    return false
  }

  /**
    * Tempt to estimate if a point is close to the line through the existing point
    */
  isCloseToLineThroughtExistingPoints (M /** GVPoint */) {
    const listExistingPoints = this.getListObjectTypeSelect('Point')
    const litsExistingLine = this.getListObjectTypeSelect('Line')
    // const numberOfPoints = listExistingPoints.length
    const numberOfObjects = listExistingPoints.length + litsExistingLine.length
    const result = []
    const minDimension = Math.min(this.height, this.width) / numberOfObjects / 3
    for (let i = 0; i < listExistingPoints.length; i++) {
      for (let j = i + 1; j < listExistingPoints.length; j++) {
        const d = new GVLine(listExistingPoints[i], listExistingPoints[j])
        result.push(this.distance(M, d))
      }
    }
    for (let i = 0; i < litsExistingLine.length; i++) {
      result.push(this.distance(M, litsExistingLine[i]))
    }
    return result.some(x => x < minDimension && parseFloat(x.toFixed(15)) !== 0)
  }

  /**
    * Add a new line to the view with new name
    */
  addLine (P1 = this.addPoint()[0], P2 = this.addPoint()[0]) {
    const line = new GVLine(P1, P2)
    line.name = this.getNewName(line.type)
    this.geometric.push(line)
    return line
  }

  /**
    * Add a new Segment to the view with new name
    */
  addSegment (P1 = this.addPoint()[0], P2 = this.addPoint()[0]) {
    const segment = new GVSegment(P1, P2)
    this.geometric.push(segment)
    return segment
  }

  /**
    * Add a new circle center
    * @param {GVPoint} C
    * @param {GVPoint} P
    * @returns {GVCircle}
    */
  addCircle (C = this.addPoint()[0], X /** GVPoint | number */)/** GVCircle */ {
    const circle = new GVCircle(C, X)
    circle.name = this.getNewName(circle.type)
    this.geometric.push(circle)
    return circle
  }

  /**
    * Get the intersect point of a line and the bordure
    */
  getExtremPointGraphicLine (L/** GVLine */) {
    const x = [
      [L.getXPoint(this.ymin), this.ymin], // [xmin,xmax]
      [L.getXPoint(this.ymax), this.ymax] // [xmin,xmax]
    ]
    const y = [
      [this.xmin, L.getYPoint(this.xmin)], // [ymin,ymax]
      [this.xmax, L.getYPoint(this.xmax)] // [ymin,ymax]
    ]
    const extremites = []
    for (const u of x) {
      if (u.every(v => v !== undefined) && u[0] >= this.xmin && u[0] <= this.xmax) { extremites.push(u) }
    }
    for (const u of y) {
      if (u.every(v => v !== undefined) && u[1] >= this.ymin && u[1] <= this.ymax) { extremites.push(u) }
    }
    if (extremites.length === 2) {
      return [
        new GVPoint(new GVCartesian(extremites[0][0], extremites[0][1])),
        new GVPoint(new GVCartesian(extremites[1][0], extremites[1][1]))
      ]
    } else {
      return undefined
    }
  }

  /**
    * get a point between two points
    * @param {GVPoint} point1
    * @param {GVPoint} point2
    * @returns {GVPoint}
    */
  getNewPointBetween (A, B) {
    const k = Math.random()
    return new GVPoint(
      new GVCartesian(
        (A.x - B.x) * k + B.x,
        (A.y - B.y) * k + B.y
      )
    )
  }

  /**
    * Add point between two but not too close to extrems
    * @param {GVPoint} A
    * @param {GVPoint} B
    * @returns {GVPoint}
    */
  addPointBetween (A /** GVPoint */, B /** GVPoint */) /** GVPoint */ {
    const barycentricsCoords = listeEntiersSommeConnue(2, 100, 15)
    const P = barycentre([A, B], barycentricsCoords)
    P.name = P.name || this.getNewName(P.type)
    this.geometric.push(P)
    return P
  }

  addPointDistance (A /** GVPoint */, r /** number */) {
    let P /** GVPoint */
    const circle = new GVCircle(A, r)
    do {
      const theta = Math.random() * Math.PI * 2
      P = circle.getPoint(theta)
    } while (this.isCloseToExistingPoints(P) || this.isCloseToLineThroughtExistingPoints(P))
    P.name = this.getNewName(P.type)
    this.geometric.push(P)
    return P
  }

  addPointInPolygon (...args /** GVPoint[] */) {
    const barycentricsCoords = listeEntiersSommeConnue(args.length, 100, 20 * 3 / args.length)
    const P = barycentre(args, barycentricsCoords)
    P.name = P.name || this.getNewName(P.type)
    this.geometric.push(P)
    return P
  }

  addPointOutPolygon (...args /** GVPoint[] */) /** GVPoint */ {
    const barycentricsCoords = listeEntiersSommeConnue(args.length, 100, 20 * 3 / args.length)
    const aleaI = Math.round(Math.random() * (barycentricsCoords.length - 2))
    const P = new GVLine(args[aleaI], args[aleaI + 1]).getSymetric(barycentre(args, barycentricsCoords))
    P.name = P.name || this.getNewName(P.type)
    this.geometric.push(P)
    return P
  }

  addPointOnPolygon (...args /** GVPoint[] */) /** GVPoint */ {
    const barycentricsCoords = listeEntiersSommeConnue(2, 100, 20 * 3 / 2)
    const P = barycentre(circularPermutation(args).slice(0, 2), barycentricsCoords)
    P.name = P.name || this.getNewName(P.type)
    this.geometric.push(P)
    return P
    /*
     const barycentricsCoords = listeEntiersSommeConnue(args.length,100,20*3/args.length)
     barycentricsCoords[Math.round(Math.random()*(barycentricsCoords.length-2))] = 0
     const P = barycentre(args,barycentricsCoords)
     P.name = P.name || this.getNewName(P.type)
     this.geometric.push(P)
     return P
     */
  }

  placeLabelsPolygon (...args /** GVPoint[] */) {
    for (let i = 1; i < args.length + 1; i++) {
      const names = [args[args.length - 1]].concat(args).concat([args[0]])
      names[i].showName()
      names[i].labelPoints = [names[i - 1], names[i], names[i + 1]]
    }
  }

  addSymetric (X /** GVPoint | GVLine */, ...args /** GVPoint[] */) /** GVPoint[] */ {
    return args.map(x => {
      const P = X.getSymetric(x)
      P.name = P.name || this.getNewName(P.type)
      this.geometric.push(P)
      return P
    })
  }

  addTranslate (V/** GVVector */, ...args /** GVPoint[] */) /** GVPoint[] */ {
    return args.map(X => {
      const P = X.add(V)
      P.name = P.name || this.getNewName(P.type)
      this.geometric.push(P)
      return P
    })
  }

  move (V/** GVVetor */, ...args /** GVPoint[] */) {
    for (const X of args) {
      X.x = X.add(V).x
      X.y = X.add(V).y
    }
  }

  /**
    * Add three point, two point or one point aligned to others
    * @param  {Point} P1 // If no point or one point we creat new points
    * @param  {Point} P2 // If no point or one point we creat new points
    * @returns {GVPoint[]}
    */
  addPointAligned (P1 = this.addPoint()[0], P2 = this.addPoint()[0])/** GVPoint[] */ {
    let P3
    do {
      const line = new GVLine(P1, P2)
      const [X1, X2] = this.getExtremPointGraphicLine(line)
      P3 = this.getNewPointBetween(X1, X2)
    } while (this.isCloseToExistingPoints(P3) || this.isCloseToLineThroughtExistingPoints(P3))
    P3.name = P3.name || this.getNewName(P3.type)
    this.geometric.push(P3)
    return [P1, P2, P3]
  }

  /**
    * P1, P2, P3 with P2P1P3 rectangular in P1
    * @param args
    * @returns {GVPoint[]}
    */
  addRectPoint (...args) /** GVPoint[] */ {
    let P3, P1, P2
    do {
      if (P1 !== undefined) {
        for (let i = 0; i < 2 - args.length; i++) {
          this.geometric.pop()
          this.geometric.pop()
        }
      }
      [P1, P2] = args.concat(this.addPoint(2 - args.length))
      const line = (new GVLine(P1, P2)).getPerpendicularLine(P1)
      const [X1, X2] = this.getExtremPointGraphicLine(line)
      P3 = this.getNewPointBetween(X1, X2)
    } while (this.isCloseToExistingPoints(P3) || this.isCloseToLineThroughtExistingPoints(P3))
    P3.name = P3.name || this.getNewName(P3.type)
    this.geometric.push(P3)
    return [P1, P2, P3]
  }

  /**
    * Distances to the sides of a triangle
    * @param  {GVPoint} P1
    * @param  {GVPoint} P2
    * @param  {GVPoint} P3
    * @returns {number}
    */
  distanceMinSidesVertices (P1 /** GVPoint */, P2 /** GVPoint */, P3 /** GVPoint */) /** number */ {
    // A faire pour n'importe quel nombre de sommets ?
    return Math.min(
      this.distance(P1, new GVLine(P2, P3)),
      this.distance(P2, new GVLine(P1, P3)),
      this.distance(P3, new GVLine(P1, P2))
    )
  }

  /**
    * Add three points not aligned or one not aligned with the two others
    * @param  {GVPoint} P1 If no point we create three new points
    * @param  {GVPoint} P2 If no point we create three new points
    * @param  {GVPoint} P3 If no point we create three new points
    * @returns {GVPoint}
    */
  addNotAlignedPoint (P1 = this.addPoint()[0], P2 = this.addPoint()[0], P3 /** GVPoint */) /** GVPoint[] */ {
    // Le troisième point est écrasé si existant
    // Réfléchir à un ensemble plus grand de points non alignés
    const minDimension = Math.min(this.height, this.width) / this.getListObjectTypeSelect('Point').length / 3
    do {
      if (P3 !== undefined) this.geometric.pop()
      P3 = this.addPoint()[0]
    } while (this.distanceMinSidesVertices(P1, P2, P3) < minDimension)
    P3.name = P3.name || this.getNewName(P3.type)
    return [P1, P2, P3]
  }

  /**
    * Add a parallel line to another one or two parallel lines
    * @param  {GVPoint} P If no args we create two parallels
    * @param  {GVLine} line If no args we create two parallels
    * @returns {GVLine}
    */
  addParallelLine (P = this.addPoint()[0], line = this.addLine()) {
    const parallel = new GVLine(P, line.direction)
    parallel.name = this.getNewName(parallel.type)
    this.geometric.push(parallel, P, line)
    return [line, parallel]
  }

  addPerpendicularLine (P = this.addPoint()[0], line = this.addLine()) {
    const perpendicular = new GVLine(P, line.direction.getNormal())
    perpendicular.name = this.getNewName(perpendicular.type)
    this.geometric.push(perpendicular)
    return [line, perpendicular]
  }

  /**
    * Add the sides of a polygon
    * @param  {...any} args
    * @returns {}
    */
  addSidesPolygon (...args)/** GVSegment[] */ {
    const sides = []
    for (let i = 0; i < args.length - 1; i++) {
      // sides.push(this.addSegment(args[i], args[i + 1]))
      sides.push(this.addSegment(args[i], args[i + 1]))
    }
    sides.push(this.addSegment(args[args.length - 1], args[0]))
    return sides
  }

  /**
    * Add labels to the vertices of a polygon.
    * @param args
    */
  addLabelsPointsPolygon (...args /** GVPoint[] */) {
    const last = args.length - 1
    const vertices = [args[last]].concat(args).concat(args[0])
    for (let i = 1; i < args.length + 1; i++) {
      vertices[i].showName()
      vertices[i].labelPoints = [vertices[i - 1], vertices[i], vertices[i + 1]]
    }
  }

  addTriangle (arg1 /** number | GVPoint */, arg2 /** number | GVPoint */, arg3 /** number | GVPoint */, arg4 /** number */)/** GVTriangle */ {
    let triangle
    if (arg1 instanceof GVPoint && arg2 !== undefined && arg2 instanceof GVPoint && arg3 instanceof GVPoint) {
      triangle = new GVTriangle(arg1, arg2, arg3)
    } else if (arg1 instanceof GVPoint && arg2 instanceof GVPoint && arg3 instanceof GVPoint) {
      triangle = new GVTriangle(...this.addNotAlignedPoint(...[arg1, arg2, arg3].filter(P => P !== undefined)))
    } else if (arg1 instanceof GVPoint && typeof arg2 === 'number' && arg3 instanceof GVPoint && typeof arg4 === 'number') {
      const cercle1 = this.addCircle(arg1, arg2)
      const cercle2 = this.addCircle(arg3, arg4)
      const [P] = this.addIntersectLine(cercle1, cercle2)
      triangle = new GVTriangle(arg1, arg3, P)
    } else if (typeof arg1 === 'number' && typeof arg2 === 'number' && typeof arg3 === 'number') {
      const A = this.addPoint()[0]
      const B = this.addPointDistance(A, arg1)
      const cercle1 = this.addCircle(A, arg2)
      const cercle2 = this.addCircle(B, arg3)
      const [C] = this.addIntersectLine(cercle1, cercle2)
      triangle = new GVTriangle(A, B, C)
    } else if (arg1 === undefined) {
      triangle = new GVTriangle(...this.addNotAlignedPoint())
    }
    return triangle
  }

  /**
    * Add a group of 4 points making a parallelogram
    * @param  {GVPoint} A // 0-3 point
    * @param  {GVPoint} B // 0-3 point
    * @param  {GVPoint} C // 0-3 point
    * @returns {GVPolygon}
    */
  addParallelogram (A /** GVPoint */ = this.addPoint()[0], B /** GVPoint */ = this.addPoint()[0], C /** GVPoint */ = this.addNotAlignedPoint(A, B)[2], D = undefined)/** GVPolygon */ {
    D = new GVPoint(
      new GVCartesian(
        A.x + C.x - B.x,
        A.y + C.y - B.y
      )
    )
    D.name = D.name || this.getNewName(D.type)
    this.geometric.push(D)
    return new GVPolygon(A, B, C, D)
  }

  addRegularPolygon (n /** number */, A /** GVPoint */ = this.addPoint()[0], B /** GVPoint */ = this.addPoint()[0])/** GVPolygon */ {
    const points /** GVPoint[] */ = [A, B]
    for (let i = 2; i < n; i++) {
      const P = points[i - 2].getRotate(points[i - 1], Math.PI - 2 * Math.PI / n)
      P.name = P.name || this.getNewName(P.type)
      this.geometric.push(P)
      points.push(P)
    }
    return new GVPolygon(...points)
  }

  addRectangle (A /** GVPoint | number */, B /** GVPoint | number */, C /** GVPoint */) {
    let rectangle/** GVRectangle */
    if (A === undefined) {
      do {
        if (rectangle !== undefined) {
          this.geometric.pop()
          this.geometric.pop()
          this.geometric.pop()
          this.geometric.pop()
        }
        const [A, B, D] = this.addRectPoint()
        const C = this.addParallelogram(D, A, B).vertices[3]
        rectangle = new GVRectangle(A, B, C, D)
      } while (rectangle.ratio < 1.2 || rectangle.ratio > 1.7)
    }
    return rectangle
  }

  addRegularPolygonCenter (A /** GVPoint */ = this.addPoint()[0], B /** GVPoint */ = this.addPoint()[0], n /** number */) /** GVPoint */ {
    const angle = Math.PI * (1 / 2 - 1 / n)
    const coeff = 1 / (2 * Math.sin(Math.PI / n))
    const P = new GVPoint(
      new GVCartesian(
        ((A.x - B.x) * Math.cos(angle) - (A.y - B.y) * Math.sin(angle)) * coeff + B.x,
        ((A.x - B.x) * Math.sin(angle) + (A.y - B.y) * Math.cos(angle)) * coeff + B.y
      ))
    P.name = P.name || this.getNewName(P.type)
    this.geometric.push(P)
    return P
  }

  addHomothetic (O /** GVPoint */, k /** number */, ...args /** GVPoint[] */) /** GVPoint[] */ {
    const homotheticPoints = []
    args.map(M => {
      const point = new GVPoint(
        new GVCartesian(
          k * M.x + (1 - k) * O.x,
          k * M.y + (1 - k) * O.y
        ))
      point.name = point.name || this.getNewName(point.type)
      this.geometric.push(point)
      homotheticPoints.push(point)
      return point
    })
    return homotheticPoints
  }

  /**
      * Add the angle ABC to the graphic view
      * @param {Point} A
      * @param {Point} B
      * @param {Point} C
      */
  addAngle (A /** GVPoint */, B /** GVPoint */, C /** GVPoint */) {
    const newAngle = new GVAngle(A, B, C)
    this.geometric.push(newAngle)
    return newAngle
  }

  addAnglesPolygon (...args /** GVPoint[] */)/** GVAngle[] */ {
    const last = args.length - 1
    const vertices = [args[last]].concat(args).concat(args[0])
    const angles/** GVAngle[] */ = []
    for (let i = 1; i < args.length + 1; i++) {
      const newAngle = new GVAngle(vertices[i - 1], vertices[i], vertices[i + 1])
      angles.push(newAngle)
      this.geometric.push(newAngle)
    }
    return angles
  }

  /**
    * Rotate points
    * @param {Point} center
    * @param {number} angle // Angle in radians
    * @param {Point} args
    * @returns {Point[]}
    * @example
    * this.addRotate(O, Math.PI()/2, B)
    */
  addRotate (center /** GVPoint */, angle /** number */, ...args /** GVPoint[] */) /** GVPoint[] */ {
    const rotatePoints = []
    args.map(M => {
      const point = new GVPoint(
        new GVCartesian(
          (M.x - center.x) * Math.cos(angle) - (M.y - center.y) * Math.sin(angle) + center.x,
          (M.x - center.x) * Math.sin(angle) + (M.y - center.y) * Math.cos(angle) + center.y
        ))
      point.name = point.name || this.getNewName(point.type)
      this.geometric.push(point)
      rotatePoints.push(point)
      return point
    })
    return rotatePoints
  }

  exportGGB (arg = this.geometric) {
    const ggb = []
    arg.forEach(x => {
      ggb.push(x.getGGB())
    })
    return ggb.join('\n')
  }

  /**
    * Export to Mathalea2D
    * @returns {Mathalea2D}
    */
  getFigure (...args) {
    this.geometricExport = this.show(...args)
    return getMathalea2DExport(this)
  }
}