/**
 * @module Shape
 * @submodule Custom Shapes
 * @for p5
 */

// REMINDER: remove .js extension (currently using it to run file locally)
import { Color } from '../color/p5.Color';
import { Vector } from '../math/p5.Vector';
import * as constants from '../core/constants';

// ---- UTILITY FUNCTIONS ----
function polylineLength(vertices) {
  let length = 0;
  for (let i = 1; i < vertices.length; i++) {
    length += vertices[i-1].position.dist(vertices[i].position);
  }
  return length;
}

// ---- GENERAL BUILDING BLOCKS ----

class Vertex {
  constructor(properties) {
    Object.assign(this, properties);
  }
  /*
  get array() {
    // convert to 1D array
    // call `toArray()` if value is an object with a toArray() method
    // handle primitive values separately
    // maybe handle object literals too, with Object.values()?
    // probably don’t need anything else for now?
  }
  */
  // TODO: make sure name of array conversion method is
  // consistent with any modifications to the names of corresponding
  // properties of p5.Vector and p5.Color
}

class ShapePrimitive {
  vertices;
  _shape = null;
  _primitivesIndex = null;
  _contoursIndex = null;
  isClosing = false;

  constructor(...vertices) {
    if (new.target === ShapePrimitive) {
      throw new Error('ShapePrimitive is an abstract class: it cannot be instantiated.');
    }
    if (vertices.length > 0) {
      this.vertices = vertices;
    }
    else {
      throw new Error('At least one vertex must be passed to the constructor.');
    }
  }

  get vertexCount() {
    return this.vertices.length;
  }

  get vertexCapacity() {
    throw new Error('Getter vertexCapacity must be implemented.');
  }

  get _firstInterpolatedVertex() {
    return this.startVertex();
  }

  get canOverrideAnchor() {
    return false;
  }

  accept(visitor) {
    throw new Error('Method accept() must be implemented.');
  }

  addToShape(shape) {
    /*
    TODO:
    Refactor?
    Test this method once more primitives are implemented.
    Test segments separately (Segment adds an extra step to this method).
    */
    let lastContour = shape.at(-1);

    if (lastContour.primitives.length === 0) {
      lastContour.primitives.push(this);
    } else {
      // last primitive in shape
      let lastPrimitive = shape.at(-1, -1);
      let hasSameType = lastPrimitive instanceof this.constructor;
      let spareCapacity = lastPrimitive.vertexCapacity -
                          lastPrimitive.vertexCount;

      // this primitive
      let pushableVertices;
      let remainingVertices;

      if (hasSameType && spareCapacity > 0) {

        pushableVertices = this.vertices.splice(0, spareCapacity);
        remainingVertices = this.vertices;
        lastPrimitive.vertices.push(...pushableVertices);

        if (remainingVertices.length > 0) {
          lastContour.primitives.push(this);
        }
      }
      else {
        lastContour.primitives.push(this);
      }
    }

    // if primitive itself was added
    // (i.e. its individual vertices weren't all added to an existing primitive)
    // give it a reference to the shape and store its location within the shape
    let addedToShape = this.vertices.length > 0;
    if (addedToShape) {
      let lastContour = shape.at(-1);
      this._primitivesIndex = lastContour.primitives.length - 1;
      this._contoursIndex = shape.contours.length - 1;
      this._shape = shape;
    }

    return shape.at(-1, -1);
  }

  get _nextPrimitive() {
    return this._belongsToShape ?
      this._shape.at(this._contoursIndex, this._primitivesIndex + 1) :
      null;
  }

  get _belongsToShape() {
    return this._shape !== null;
  }

  handlesClose() {
    return false;
  }

  close(vertex) {
    throw new Error('Unimplemented!');
  }
}

class Contour {
  #kind;
  primitives;

  constructor(kind = constants.PATH) {
    this.#kind = kind;
    this.primitives = [];
  }

  get kind() {
    const isEmpty = this.primitives.length === 0;
    const isPath = this.#kind === constants.PATH;
    return isEmpty && isPath ? constants.EMPTY_PATH : this.#kind;
  }

  accept(visitor) {
    for (const primitive of this.primitives) {
      primitive.accept(visitor);
    }
  }
}

// ---- PATH PRIMITIVES ----

class Anchor extends ShapePrimitive {
  get vertexCapacity() {
    return 1;
  }

  accept(visitor) {
    visitor.visitAnchor(this);
  }

  getEndVertex() {
    return this.vertices[0];
  }
}

// abstract class
class Segment extends ShapePrimitive {
  constructor(...vertices) {
    super(...vertices);
    if (new.target === Segment) {
      throw new Error('Segment is an abstract class: it cannot be instantiated.');
    }
  }

  // segments in a shape always have a predecessor
  // (either an anchor or another segment)
  get _previousPrimitive() {
    return this._belongsToShape ?
      this._shape.at(this._contoursIndex, this._primitivesIndex - 1) :
      null;
  }

  getStartVertex() {
    return this._previousPrimitive.getEndVertex();
  }

  getEndVertex() {
    return this.vertices[this.vertices.length - 1];
  }
}

class LineSegment extends Segment {
  // Consecutive line vertices on a path batch into a single LineSegment
  // (see Shape.vertex()), so vertexCount may exceed this capacity. The
  // capacity only governs addToShape() merging, which batching bypasses;
  // it stays at 1 so the generic path never merges into a LineSegment
  // (a closing vertex must remain its own segment).
  get vertexCapacity() {
    return 1;
  }

  accept(visitor) {
    visitor.visitLineSegment(this);
  }
}

class BezierSegment extends Segment {
  #order;
  #vertexCapacity;

  constructor(order, ...vertices) {
    super(...vertices);

    // Order m may sometimes be passed as an array [m], since arrays
    // may be used elsewhere to store order of
    // Bezier curves and surfaces in a common format

    let numericalOrder = Array.isArray(order) ? order[0] : order;
    this.#order = numericalOrder;
    this.#vertexCapacity = numericalOrder;
  }

  get order() {
    return this.#order;
  }

  get vertexCapacity() {
    return this.#vertexCapacity;
  }

  #_hullLength;
  hullLength() {
    if (this.#_hullLength === undefined) {
      this.#_hullLength = polylineLength([
        this.getStartVertex(),
        ...this.vertices
      ]);
    }
    return this.#_hullLength;
  }

  accept(visitor) {
    visitor.visitBezierSegment(this);
  }
}

/*
To-do: Consider type and end modes -- see #6766
may want to use separate classes, but maybe not

For now, the implementation overrides
super.getEndVertex() in order to preserve current p5
endpoint behavior, but we're considering defaulting
to interpolated endpoints (a breaking change)
*/
class SplineSegment extends Segment {
  #vertexCapacity = Infinity;
  _splineProperties = {
    ends: constants.INCLUDE,
    tightness: 0
  };

  get vertexCapacity() {
    return this.#vertexCapacity;
  }

  accept(visitor) {
    visitor.visitSplineSegment(this);
  }

  get _comesAfterSegment() {
    return this._previousPrimitive instanceof Segment;
  }

  get canOverrideAnchor() {
    return this._splineProperties.ends === constants.EXCLUDE;
  }

  // assuming for now that the first interpolated vertex is always
  // the second vertex passed to splineVertex()
  // if this spline segment doesn't follow another segment,
  // the first vertex is in an anchor
  get _firstInterpolatedVertex() {
    if (this._splineProperties.ends === constants.EXCLUDE) {
      return this._comesAfterSegment ?
        this.vertices[1] :
        this.vertices[0];
    } else {
      return this.getStartVertex();
    }
  }

  get _chainedToSegment() {
    if (this._belongsToShape && this._comesAfterSegment) {
      let interpolatedStartPosition = this._firstInterpolatedVertex.position;
      let predecessorEndPosition = this.getStartVertex().position;
      return predecessorEndPosition.equals(interpolatedStartPosition);
    }
    else {
      return false;
    }
  }

  // extend addToShape() with a warning in case second vertex
  // doesn't line up with end of last segment
  addToShape(shape) {
    const added = super.addToShape(shape);
    this._splineProperties.ends = shape._splineProperties.ends;
    this._splineProperties.tightness = shape._splineProperties.tightness;

    if (this._splineProperties.ends !== constants.EXCLUDE) return added;

    let verticesPushed = !this._belongsToShape;
    let lastPrimitive = shape.at(-1, -1);

    let message = (array1, array2) =>
      `Spline does not start where previous path segment ends:
      second spline vertex at (${array1})
      expected to be at (${array2}).`;

    if (verticesPushed &&
      // Only check once the first interpolated vertex has been added
      lastPrimitive.vertices.length === 2 &&
      lastPrimitive._comesAfterSegment &&
      !lastPrimitive._chainedToSegment
    ) {
      let interpolatedStart = lastPrimitive._firstInterpolatedVertex.position;
      let predecessorEnd = lastPrimitive.getStartVertex().position;

      console.warn(
        message(interpolatedStart.array(), predecessorEnd.array())
      );
    }

    // Note: Could add a warning in an else-if case for when this spline segment
    // is added directly to the shape instead of pushing its vertices to
    // an existing spline segment. However, if we assume addToShape() is called by
    // splineVertex(), it'd add a new spline segment with only one vertex in that case,
    // and the check wouldn't be needed yet.

    // TODO: Consider case where positions match but other vertex properties don't.
    return added;
  }

  // override method on base class
  getEndVertex() {
    if (this._splineProperties.ends === constants.INCLUDE) {
      return super.getEndVertex();
    } else if (this._splineProperties.ends === constants.EXCLUDE) {
      return this.vertices.at(-2);
    } else {
      return this.getStartVertex();
    }
  }

  getControlPoints() {
    let points = [];

    if (this._comesAfterSegment) {
      points.push(this.getStartVertex());
    }
    points.push(this.getStartVertex());

    for (const vertex of this.vertices) {
      points.push(vertex);
    }

    const prevVertex = this.getStartVertex();
    if (this._splineProperties.ends === constants.INCLUDE) {
      points.unshift(prevVertex);
      points.push(this.vertices.at(-1));
    } else if (this._splineProperties.ends === constants.JOIN) {
      points.unshift(this.vertices.at(-1));
      points.push(prevVertex, this.vertices.at(0));
    }

    return points;
  }

  handlesClose() {
    if (!this._belongsToShape) return false;

    // Only handle closing if the spline is the only thing in its contour after
    // the anchor
    const contour = this._shape.at(this._contoursIndex);
    return contour.primitives.length === 2 && this._primitivesIndex === 1;
  }

  close() {
    this._splineProperties.ends = constants.JOIN;
  }
}

// ---- ISOLATED PRIMITIVES ----

class Point extends ShapePrimitive {
  get vertexCapacity() {
    return 1;
  }

  accept(visitor) {
    visitor.visitPoint(this);
  }
}

class Line extends ShapePrimitive {
  get vertexCapacity() {
    return 2;
  }

  accept(visitor) {
    visitor.visitLine(this);
  }
}

class Triangle extends ShapePrimitive {
  get vertexCapacity() {
    return 3;
  }

  accept(visitor) {
    visitor.visitTriangle(this);
  }
}

class Quad extends ShapePrimitive {
  get vertexCapacity() {
    return 4;
  }

  accept(visitor) {
    visitor.visitQuad(this);
  }
}

/*
 * TODO: Future enhancement — align with arcVertex proposal (#6459)
 * Currently stores start/stop angles and mode (OPEN/CHORD/PIE).
 * For full SVG compatibility and arcs inside beginShape/endShape,
 * we may want to add an arc-to-vertex variant that matches the
 * arcVertex() API discussed in #6459.
 */

class ArcPrimitive extends ShapePrimitive {
  #x;
  #y;
  #w;
  #h;
  #start;
  #stop;
  #mode;
  #vertexCapacity = 2;

  constructor(startVertex, endVertex, x, y, w, h, start, stop, mode) {
    // ShapePrimitive requires at least one vertex; pass a placeholder
    super(startVertex, endVertex);
    this.#x = x;
    this.#y = y;
    this.#w = w;
    this.#h = h;
    this.#start = start;
    this.#stop = stop;
    this.#mode = mode;
  }

  get x() { return this.#x; }
  get y() { return this.#y; }
  get w() { return this.#w; }
  get h() { return this.#h; }
  get start() { return this.#start; }
  get stop() { return this.#stop; }
  get mode() { return this.#mode; }
  get startVertex() { return this.vertices[0]; }
  get endVertex() { return this.vertices[1]; }

  get vertexCapacity() {
    return this.#vertexCapacity;
  }

  accept(visitor) {
    visitor.visitArcPrimitive(this);
  }
}

class EllipsePrimitive extends ShapePrimitive {
  #x;
  #y;
  #w;
  #h;
  #vertexCapacity = 1;

  constructor(centerVertex, x, y, w, h) {

    super(centerVertex);
    this.#x = x;
    this.#y = y;
    this.#w = w;
    this.#h = h;
  }

  get x() { return this.#x; }
  get y() { return this.#y; }
  get w() { return this.#w; }
  get h() { return this.#h; }

  get vertexCapacity() {
    return this.#vertexCapacity;
  }

  accept(visitor) {
    visitor.visitEllipsePrimitive(this);
  }
}

class RectPrimitive extends ShapePrimitive {
  #x;
  #y;
  #w;
  #h;
  #tl;
  #tr;
  #br;
  #bl;
  #vertexCapacity = 1;

  constructor(startVertex, x, y, w, h, tl, tr, br, bl) {
    super(startVertex);
    this.#x = x;
    this.#y = y;
    this.#w = w;
    this.#h = h;
    this.#tl = tl;
    this.#tr = tr;
    this.#br = br;
    this.#bl = bl;
  }

  get x() { return this.#x; }
  get y() { return this.#y; }
  get w() { return this.#w; }
  get h() { return this.#h; }
  get tl() { return this.#tl; }
  get tr() { return this.#tr; }
  get br() { return this.#br; }
  get bl() { return this.#bl; }

  get vertexCapacity() {
    return this.#vertexCapacity;
  }

  accept(visitor) {
    visitor.visitRectPrimitive(this);
  }
}

// ---- TESSELLATION PRIMITIVES ----

class TriangleFan extends ShapePrimitive {
  get vertexCapacity() {
    return Infinity;
  }

  accept(visitor) {
    visitor.visitTriangleFan(this);
  }
}

class TriangleStrip extends ShapePrimitive {
  get vertexCapacity() {
    return Infinity;
  }

  accept(visitor) {
    visitor.visitTriangleStrip(this);
  }
}

class QuadStrip extends ShapePrimitive {
  get vertexCapacity() {
    return Infinity;
  }

  accept(visitor) {
    visitor.visitQuadStrip(this);
  }
}

// ---- PRIMITIVE SHAPE CREATORS ----

// Creators are stored in a static nested object keyed by vertex kind and
// then by shape kind, so a lookup is two property accesses with no key
// string to build, and nothing is constructed per Shape. Shape kinds are
// primitive constants (numbers/strings), which work as computed keys;
// Symbols would too, if constants become Symbols later.
const defaultPrimitiveShapeCreators = {
  vertex: {
    [constants.EMPTY_PATH]: (...vertices) => new Anchor(...vertices),
    [constants.PATH]: (...vertices) => new LineSegment(...vertices),
    [constants.POINTS]: (...vertices) => new Point(...vertices),
    [constants.LINES]: (...vertices) => new Line(...vertices),
    [constants.TRIANGLES]: (...vertices) => new Triangle(...vertices),
    [constants.QUADS]: (...vertices) => new Quad(...vertices),
    [constants.TRIANGLE_FAN]: (...vertices) => new TriangleFan(...vertices),
    [constants.TRIANGLE_STRIP]: (...vertices) => new TriangleStrip(...vertices),
    [constants.QUAD_STRIP]: (...vertices) => new QuadStrip(...vertices)
  },
  // bezierVertex creators all take order and vertices so they can be
  // called in a uniform way
  bezierVertex: {
    [constants.EMPTY_PATH]: (order, ...vertices) => new Anchor(...vertices),
    [constants.PATH]: (order, ...vertices) =>
      new BezierSegment(order, ...vertices)
  },
  splineVertex: {
    [constants.EMPTY_PATH]: (...vertices) => new Anchor(...vertices),
    [constants.PATH]: (...vertices) => new SplineSegment(...vertices)
  }
};

// ---- SHAPE ----

/* Note: It's assumed that Shape instances are always built through
 * their beginShape()/endShape() methods. For example, this ensures
 * that a segment is never the first primitive in a contour (paths
 * always start with an anchor), which simplifies code elsewhere.
 */
class Shape {
  #vertexProperties;
  #initialVertexProperties;
  #primitiveShapeCreators;
  #bezierOrder = 3;
  kind = null;
  contours = [];
  _splineProperties = {
    tightness: 0,
    ends: constants.INCLUDE
  };
  userVertexProperties = null;

  constructor(
    vertexProperties,
    primitiveShapeCreators = defaultPrimitiveShapeCreators
  ) {
    this.#initialVertexProperties = vertexProperties;
    this.#vertexProperties = vertexProperties;
    this.#primitiveShapeCreators = primitiveShapeCreators;

    for (const key in this.#vertexProperties) {
      if (key !== 'position' && key !== 'textureCoordinates') {
        this[key] = function(value) {
          this.#vertexProperties[key] = value;
        };
      }
    }
  }

  serializeToArray(val) {
    if (val === null || val === undefined) {
      return [];
    } if (val instanceof Number) {
      return [val];
    } else if (val instanceof Array) {
      return val;
    } else if (val.array instanceof Function) {
      return val.array();
    } else {
      throw new Error(`Can't convert ${val} to array!`);
    }
  }

  vertexToArray(vertex) {
    const array = [];
    for (const key in this.#vertexProperties) {
      if (this.userVertexProperties && key in this.userVertexProperties)
        continue;
      const val = vertex[key];
      array.push(...this.serializeToArray(val));
    }
    for (const key in this.userVertexProperties) {
      if (key in vertex) {
        array.push(...this.serializeToArray(vertex[key]));
      } else {
        array.push(...new Array(this.userVertexProperties[key]).fill(0));
      }
    }
    return array;
  }

  hydrateValue(queue, original) {
    if (original === null) {
      return null;
    } else if (original instanceof Number) {
      return queue.shift();
    } else if (original instanceof Array) {
      const array = [];
      for (let i = 0; i < original.length; i++) {
        array.push(queue.shift());
      }
      return array;
    } else if (original instanceof Vector) {
      return new Vector(queue.shift(), queue.shift(), queue.shift());
    } else if (original instanceof Color) {
      // NOTE: Not sure what intention here is, `Color` constructor signature
      // has changed so needed to be reviewed
      const array = [
        queue.shift(),
        queue.shift(),
        queue.shift(),
        queue.shift()
      ];
      return new Color(array);
    }
  }

  arrayToVertex(array) {
    const vertex = {};
    const queue = [...array];

    for (const key in this.#vertexProperties) {
      if (this.userVertexProperties && key in this.userVertexProperties)
        continue;
      const original = this.#vertexProperties[key];
      vertex[key] = this.hydrateValue(queue, original);
    }
    for (const key in this.userVertexProperties) {
      const original = this.#vertexProperties[key];
      vertex[key] = this.hydrateValue(queue, original);
    }
    return vertex;
  }

  arrayScale(array, scale) {
    return array.map(v => v * scale);
  }

  arraySum(first, ...rest) {
    return first.map((v, i) => {
      let result = v;
      for (let j = 0; j < rest.length; j++) {
        result += rest[j][i];
      }
      return result;
    });
  }

  arrayMinus(a, b) {
    return a.map((v, i) => v - b[i]);
  }

  evaluateCubicBezier([a, b, c, d], t) {
    return this.arraySum(
      this.arrayScale(a, Math.pow(1 - t, 3)),
      this.arrayScale(b, 3 * Math.pow(1 - t, 2) * t),
      this.arrayScale(c, 3 * (1 - t) * Math.pow(t, 2)),
      this.arrayScale(d, Math.pow(t, 3))
    );
  }

  evaluateQuadraticBezier([a, b, c], t) {
    return this.arraySum(
      this.arrayScale(a, Math.pow(1 - t, 2)),
      this.arrayScale(b, 2 * (1 - t) * t),
      this.arrayScale(c, t * t)
    );
  }

  /*
  catmullRomToBezier(vertices, tightness)

  Abbreviated description:
  Converts a Catmull-Rom spline to a sequence of Bezier curveTo points.

  Parameters:
  vertices -> Array [v0, v1, v2, v3, ...] of at least four vertices
  tightness -> Number affecting shape of curve

  Returns:
  array of Bezier curveTo control points, each represented as [c1, c2, c3][]

  TODO:
  1. It seems p5 contains code for converting from Catmull-Rom to Bezier in at least two places:

  catmullRomToBezier() is based on code in the legacy endShape() function:
  https://github.com/processing/p5.js/blob/1b66f097761d3c2057c0cec4349247d6125f93ca/src/core/p5.Renderer2D.js#L859C1-L886C1

  A different conversion can be found elsewhere in p5:
  https://github.com/processing/p5.js/blob/17304ce9e9ef3f967bd828102a51b62a2d39d4f4/src/typography/p5.Font.js#L1179

  A more careful review and comparison of both implementations would be helpful. They're different. I put
  catmullRomToBezier() together quickly without checking the math/algorithm, when I made the proof of concept
  for the refactor.

  2. It may be possible to replace the code in p5.Font.js with the code here, to reduce duplication.
  */
  catmullRomToBezier(vertices, tightness) {
    let s = 1 - tightness;
    let bezArrays = [];

    for (let i = 0; i + 3 < vertices.length; i++) {
      const [a, b, c, d] = vertices.slice(i, i + 4);
      const bezB = this.arraySum(
        b,
        this.arrayScale(this.arrayMinus(c, a), s / 6)
      );
      const bezC = this.arraySum(
        c,
        this.arrayScale(this.arrayMinus(b, d), s / 6)
      );
      const bezD = c;

      bezArrays.push([bezB, bezC, bezD]);
    }
    return bezArrays;
  }

  // TODO for at() method:

  // RENAME?
  // -at() indicates it works like Array.prototype.at(), e.g. with negative indices
  // -get() may work better if we want to add a corresponding set() method
  // -a set() method could maybe check for problematic usage (e.g. inserting a Triangle into a PATH)
  // -renaming or removing would necessitate changes at call sites (it's already in use)

  // REFACTOR?

  // TEST
  at(contoursIndex, primitivesIndex, verticesIndex) {
    let contour;
    let primitive;

    contour = this.contours.at(contoursIndex);

    switch(arguments.length) {
      case 1:
        return contour;
      case 2:
        return contour.primitives.at(primitivesIndex);
      case 3:
        primitive = contour.primitives.at(primitivesIndex);
        return primitive.vertices.at(verticesIndex);
    }
  }

  // note: p5.Geometry has a reset() method, but also clearColors()
  // looks like reset() isn't in the public reference, so maybe we can switch
  // everything to clear()? Not sure if reset/clear is used in other classes,
  // but it'd be good if geometries and shapes are consistent
  reset() {
    this.#vertexProperties = { ...this.#initialVertexProperties };
    this.kind = null;
    this.contours = [];
    this.userVertexProperties = null;
  }

  vertexProperty(name, data) {
    this.userVertexProperties = this.userVertexProperties || {};
    const key = this.vertexPropertyKey(name);

    const dataArray = Array.isArray(data) ? data : [data];

    if (!this.userVertexProperties[key]) {
      this.userVertexProperties[key] = dataArray.length;
    }
    this.#vertexProperties[key] = dataArray;
  }
  vertexPropertyName(key) {
    return key.replace(/Src$/, '');
  }
  vertexPropertyKey(name) {
    return name + 'Src';
  }

  bezierOrder(...order) {
    this.#bezierOrder = order;
  }

  splineProperty(key, value) {
    this._splineProperties[key] = value;
  }

  splineProperties(values) {
    if (values) {
      for (const key in values) {
        this.splineProperty(key, values[key]);
      }
    } else {
      return this._splineProperties;
    }
  }

  /*
  To-do: Maybe refactor #createVertex() since this has side effects that aren't advertised
  in the method name?
  */
  #createVertex(position, textureCoordinates) {
    this.#vertexProperties.position = position;

    if (textureCoordinates !== undefined) {
      this.#vertexProperties.textureCoordinates = textureCoordinates;
    }

    return new Vertex(this.#vertexProperties);
  }

  #createPrimitiveShape(vertexKind, shapeKind, ...vertices) {
    let primitiveShapeCreator =
      this.#primitiveShapeCreators[vertexKind][shapeKind];

    return  vertexKind === 'bezierVertex' ?
      primitiveShapeCreator(this.#bezierOrder, ...vertices) :
      primitiveShapeCreator(...vertices);
  }

  /*
    #generalVertex() is reused by the special vertex functions,
    including vertex(), bezierVertex(), splineVertex(), and arcVertex():

    It creates a vertex, builds a primitive including that
    vertex, and has the primitive add itself to the shape.
  */
  #generalVertex(kind, position, textureCoordinates) {
    let vertexKind = kind;
    let lastContourKind = this.at(-1).kind;
    let vertex = this.#createVertex(position, textureCoordinates);

    let primitiveShape = this.#createPrimitiveShape(
      vertexKind,
      lastContourKind,
      vertex
    );

    return primitiveShape.addToShape(this);
  }

  vertex(position, textureCoordinates, { isClosing = false } = {}) {
    // Fast path for the most common case: appending a line segment to a
    // path that has already started. Equivalent to the general path below
    // (a LineSegment's vertex capacity is 1, so addToShape() never merges
    // it into the previous primitive), without the creator-map lookup and
    // the generic merging logic.
    const contours = this.contours;
    const lastContour = contours[contours.length - 1];
    if (
      lastContour !== undefined &&
      lastContour.primitives.length > 0 &&
      lastContour.kind === constants.PATH
    ) {
      const vertex = this.#createVertex(position, textureCoordinates);
      const primitives = lastContour.primitives;
      const lastPrimitive = primitives[primitives.length - 1];
      if (
        !isClosing &&
        lastPrimitive instanceof LineSegment &&
        !lastPrimitive.isClosing
      ) {
        // Consecutive line vertices accumulate into one polyline segment
        lastPrimitive.vertices.push(vertex);
        return;
      }
      const segment = new LineSegment(vertex);
      segment.isClosing = isClosing;
      segment._primitivesIndex = primitives.length;
      segment._contoursIndex = contours.length - 1;
      segment._shape = this;
      primitives.push(segment);
      return;
    }
    const added = this.#generalVertex('vertex', position, textureCoordinates);
    added.isClosing = isClosing;
  }

  bezierVertex(position, textureCoordinates) {
    this.#generalVertex('bezierVertex', position, textureCoordinates);
  }

  splineVertex(position, textureCoordinates) {
    this.#generalVertex('splineVertex', position, textureCoordinates);
  }

  arcVertex(position, textureCoordinates) {
    this.#generalVertex('arcVertex', position, textureCoordinates);
  }


  arcPrimitive(x,y,w,h,start,stop,mode){
    this.beginShape();
    const centerX = x+w/2;
    const centerY = y+h/2;
    const radiusX = w / 2;
    const radiusY = h / 2;

    const startVertex = this.#createVertex(
      new Vector(
        centerX + radiusX * Math.cos(start),
        centerY + radiusY * Math.sin(start)
      )
    );

    const endVertex = this.#createVertex(
      new Vector(
        centerX + radiusX * Math.cos(stop),
        centerY + radiusY * Math.sin(stop)
      )
    );

    const primitive = new ArcPrimitive(
      startVertex,
      endVertex,
      x, y, w, h,
      start,
      stop,
      mode
    );
    primitive.addToShape(this);
    this.endShape();
    return this;

  }

  ellipsePrimitive(x,y,w,h){
    const centerVertex = this.#createVertex(new Vector(x+w/2,y+h/2));

    const primitive = new EllipsePrimitive(centerVertex, x, y, w, h);
    return primitive.addToShape(this);
  }

  rectPrimitive(x, y, w, h, tl, tr, br, bl) {
    const startVertex = this.#createVertex(new Vector(x, y));
    const primitive = new RectPrimitive(startVertex, x, y, w, h, tl, tr, br, bl);
    return primitive.addToShape(this);
  }

  point(x, y) {
    const v0 = this.#createVertex(new Vector(x, y));
    const primitive = new Point(v0);
    return primitive.addToShape(this);
  }

  line(x1, y1, x2, y2) {
    const v0 = this.#createVertex(new Vector(x1, y1));
    const v1 = this.#createVertex(new Vector(x2, y2));
    const primitive = new Line(v0, v1);
    return primitive.addToShape(this);
  }

  triangle(x1, y1, x2, y2, x3, y3) {
    const v0 = this.#createVertex(new Vector(x1, y1));
    const v1 = this.#createVertex(new Vector(x2, y2));
    const v2 = this.#createVertex(new Vector(x3, y3));
    const primitive = new Triangle(v0, v1, v2);
    return primitive.addToShape(this);
  }

  quad(x1, y1, x2, y2, x3, y3, x4, y4) {
    const v0 = this.#createVertex(new Vector(x1, y1));
    const v1 = this.#createVertex(new Vector(x2, y2));
    const v2 = this.#createVertex(new Vector(x3, y3));
    const v3 = this.#createVertex(new Vector(x4, y4));
    const primitive = new Quad(v0, v1, v2, v3);
    return primitive.addToShape(this);
  }

  beginContour(shapeKind = constants.PATH) {
    if (this.at(-1)?.kind === constants.EMPTY_PATH) {
      this.contours.pop();
    }
    this.contours.push(new Contour(shapeKind));
  }

  endContour(closeMode = constants.OPEN, _index = this.contours.length - 1) {
    const contour = this.at(_index);
    if (closeMode === constants.CLOSE) {
      // shape characteristics
      const isPath = contour.kind === constants.PATH;

      // anchor characteristics
      const anchorVertex = this.at(_index, 0, 0);
      const anchorHasPosition = Object.hasOwn(anchorVertex, 'position');
      const lastSegment = this.at(_index, -1);

      // close path
      if (isPath && anchorHasPosition) {
        if (lastSegment.handlesClose()) {
          lastSegment.close(anchorVertex);
        } else {
          // Temporarily remove contours after the current one so that we add to the original
          // contour again
          const rest = this.contours.splice(
            _index + 1,
            this.contours.length - _index - 1
          );
          const prevVertexProperties = this.#vertexProperties;
          this.#vertexProperties = { ...prevVertexProperties };
          for (const key in anchorVertex) {
            if (key === 'position' || key === 'textureCoordinates') continue;
            this.#vertexProperties[key] = anchorVertex[key];
          }
          this.vertex(
            anchorVertex.position,
            anchorVertex.textureCoordinates,
            { isClosing: true }
          );
          this.#vertexProperties = prevVertexProperties;
          this.contours.push(...rest);
        }
      }
    }
  }

  beginShape(shapeKind = constants.PATH) {
    this.kind = shapeKind;
    // Implicitly start a contour
    this.beginContour(shapeKind);
  }
  /* TO-DO:
     Refactor?
     - Might not need anchorHasPosition.
     - Might combine conditions at top, and rely on shortcircuiting.
     Does nothing if shape is not a path or has multiple contours. Might discuss this.
  */
  endShape(closeMode = constants.OPEN) {
    if (closeMode === constants.CLOSE) {
      // Close the first contour, the one implicitly used for shape data
      // added without an explicit contour
      this.endContour(closeMode, 0);
    }
  }

  accept(visitor) {
    for (const contour of this.contours) {
      contour.accept(visitor);
    }
  }
}

// ---- PRIMITIVE VISITORS ----

// abstract class
class PrimitiveVisitor {
  constructor() {
    if (new.target === PrimitiveVisitor) {
      throw new Error('PrimitiveVisitor is an abstract class: it cannot be instantiated.');
    }
  }
  // path primitives
  visitAnchor(anchor) {
    throw new Error('Method visitAnchor() has not been implemented.');
  }
  visitLineSegment(lineSegment) {
    throw new Error('Method visitLineSegment() has not been implemented.');
  }
  visitBezierSegment(bezierSegment) {
    throw new Error('Method visitBezierSegment() has not been implemented.');
  }
  visitSplineSegment(curveSegment) {
    throw new Error('Method visitSplineSegment() has not been implemented.');
  }
  visitArcSegment(arcSegment) {
    throw new Error('Method visitArcSegment() has not been implemented.');
  }
  visitArcPrimitive(arc) {
    throw new Error('Method visitArcPrimitive() has not been implemented.');
  }
  visitEllipsePrimitive(ellipse) {
    throw new Error('Method visitEllipsePrimitive() has not been implemented.');
  }
  visitRectPrimitive(rect) {
    throw new Error('Method visitRectPrimitive() has not been implemented.');
  }

  // isolated primitives
  visitPoint(point) {
    throw new Error('Method visitPoint() has not been implemented.');
  }
  visitLine(line) {
    throw new Error('Method visitLine() has not been implemented.');
  }
  visitTriangle(triangle) {
    throw new Error('Method visitTriangle() has not been implemented.');
  }
  visitQuad(quad) {
    throw new Error('Method visitQuad() has not been implemented.');
  }

  // tessellation primitives
  visitTriangleFan(triangleFan) {
    throw new Error('Method visitTriangleFan() has not been implemented.');
  }
  visitTriangleStrip(triangleStrip) {
    throw new Error('Method visitTriangleStrip() has not been implemented.');
  }
  visitQuadStrip(quadStrip) {
    throw new Error('Method visitQuadStrip() has not been implemented.');
  }
}

// requires testing
class PrimitiveToPath2DConverter extends PrimitiveVisitor {
  path = new Path2D();
  strokePath = null;
  fillPath = null;
  strokeWeight;
  hasFill;
  hasStroke;

  constructor({ strokeWeight, hasFill = true, hasStroke = true }) {
    super();
    this.strokeWeight = strokeWeight;
    this.hasFill = hasFill;
    this.hasStroke = hasStroke;
  }

  // path primitives
  visitAnchor(anchor) {
    let vertex = anchor.getEndVertex();
    this.path.moveTo(vertex.position.x, vertex.position.y);
  }
  visitLineSegment(lineSegment) {
    if (lineSegment.isClosing) {
      // The same as lineTo, but it adds a stroke join between this
      // and the starting vertex rather than having two caps
      this.path.closePath();
    } else {
      const vertices = lineSegment.vertices;
      for (let i = 0; i < vertices.length; i++) {
        const position = vertices[i].position;
        this.path.lineTo(position.x, position.y);
      }
    }
  }
  visitBezierSegment(bezierSegment) {
    let [v1, v2, v3] = bezierSegment.vertices;

    switch (bezierSegment.order) {
      case 2:
        this.path.quadraticCurveTo(
          v1.position.x,
          v1.position.y,
          v2.position.x,
          v2.position.y
        );
        break;
      case 3:
        this.path.bezierCurveTo(
          v1.position.x,
          v1.position.y,
          v2.position.x,
          v2.position.y,
          v3.position.x,
          v3.position.y
        );
        break;
    }
  }
  visitSplineSegment(splineSegment) {
    const shape = splineSegment._shape;

    if (
      splineSegment._splineProperties.ends === constants.EXCLUDE &&
      !splineSegment._comesAfterSegment
    ) {
      let startVertex = splineSegment._firstInterpolatedVertex;
      this.path.moveTo(startVertex.position.x, startVertex.position.y);
    }

    const arrayVertices = splineSegment.getControlPoints().map(
      v => shape.vertexToArray(v)
    );
    let bezierArrays = shape.catmullRomToBezier(
      arrayVertices,
      splineSegment._splineProperties.tightness
    ).map(arr => arr.map(vertArr => shape.arrayToVertex(vertArr)));
    for (const array of bezierArrays) {
      const points = array.flatMap(vert => [vert.position.x, vert.position.y]);
      this.path.bezierCurveTo(...points);
    }
  }
  visitPoint(point) {
    const { x, y } = point.vertices[0].position;
    this.path.moveTo(x, y);
    // Hack: to draw just strokes and not fills, draw a very very tiny line
    this.path.lineTo(x + 0.00001, y);
  }
  visitLine(line) {
    const { x: x0, y: y0 } = line.vertices[0].position;
    const { x: x1, y: y1 } = line.vertices[1].position;
    this.path.moveTo(x0, y0);
    this.path.lineTo(x1, y1);
  }
  visitTriangle(triangle) {
    const [v0, v1, v2] = triangle.vertices;
    this.path.moveTo(v0.position.x, v0.position.y);
    this.path.lineTo(v1.position.x, v1.position.y);
    this.path.lineTo(v2.position.x, v2.position.y);
    this.path.closePath();
  }
  visitQuad(quad) {
    const [v0, v1, v2, v3] = quad.vertices;
    this.path.moveTo(v0.position.x, v0.position.y);
    this.path.lineTo(v1.position.x, v1.position.y);
    this.path.lineTo(v2.position.x, v2.position.y);
    this.path.lineTo(v3.position.x, v3.position.y);
    this.path.closePath();
  }
  visitTriangleFan(triangleFan) {
    const [v0, ...rest] = triangleFan.vertices;
    for (let i = 0; i < rest.length - 1; i++) {
      const v1 = rest[i];
      const v2 = rest[i + 1];
      this.path.moveTo(v0.position.x, v0.position.y);
      this.path.lineTo(v1.position.x, v1.position.y);
      this.path.lineTo(v2.position.x, v2.position.y);
      this.path.closePath();
    }
  }
  visitTriangleStrip(triangleStrip) {
    for (let i = 0; i < triangleStrip.vertices.length - 2; i++) {
      const v0 = triangleStrip.vertices[i];
      const v1 = triangleStrip.vertices[i + 1];
      const v2 = triangleStrip.vertices[i + 2];
      this.path.moveTo(v0.position.x, v0.position.y);
      this.path.lineTo(v1.position.x, v1.position.y);
      this.path.lineTo(v2.position.x, v2.position.y);
      this.path.closePath();
    }
  }
  visitArcPrimitive(arc) {
    const centerX = arc.x + arc.w / 2;
    const centerY = arc.y + arc.h / 2;
    const radiusX = arc.w / 2;
    const radiusY = arc.h / 2;
    const startX = centerX + radiusX * Math.cos(arc.start);
    const startY = centerY + radiusY * Math.sin(arc.start);

    const delta = arc.stop - arc.start;
    const isFullCircle = Math.abs(delta % (2 * Math.PI)) < 0.00001 &&
      Math.abs(delta) > 0.00001;

    const createPieSlice = ! (
      arc.mode === constants.CHORD ||
      arc.mode === constants.OPEN ||
      isFullCircle
    );

    if (this.hasFill) {
      if (!this.fillPath) this.fillPath = new Path2D(this.path);

      this.fillPath.moveTo(startX, startY);
      this.fillPath.ellipse(centerX, centerY, radiusX, radiusY,
        0, arc.start, arc.stop);
      if (createPieSlice) {
        this.fillPath.lineTo(centerX, centerY);
      }
      this.fillPath.closePath();
    }

    if (this.hasStroke) {
      if (!this.strokePath) this.strokePath = new Path2D(this.path);

      this.strokePath.moveTo(startX, startY);
      this.strokePath.ellipse(centerX, centerY, radiusX, radiusY,
        0, arc.start, arc.stop);
      if (arc.mode === constants.PIE && createPieSlice) {
        this.strokePath.lineTo(centerX, centerY);
      }
      if (arc.mode === constants.PIE || arc.mode === constants.CHORD) {
        this.strokePath.closePath();
      }
    }

    // Clipping uses the base path rather than the specialized paint paths.
    this.path.moveTo(startX, startY);
    this.path.ellipse(centerX, centerY, radiusX, radiusY,
      0, arc.start, arc.stop);
  }
  visitEllipsePrimitive(ellipse) {
    const centerX = ellipse.x + ellipse.w / 2;
    const centerY = ellipse.y + ellipse.h / 2;
    const radiusX = ellipse.w / 2;
    const radiusY = ellipse.h / 2;

    this.path.moveTo(centerX + radiusX, centerY);
    this.path.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI);
  }
  visitRectPrimitive(rect) {
    const x = rect.x;
    const y = rect.y;
    const w = rect.w;
    const h = rect.h;
    let tl = rect.tl;
    let tr = rect.tr;
    let br = rect.br;
    let bl = rect.bl;

    if (typeof tl === 'undefined') {
      this.path.rect(x, y, w, h);
    } else {
      if (typeof tr === 'undefined') {
        tr = tl;
      }
      if (typeof br === 'undefined') {
        br = tr;
      }
      if (typeof bl === 'undefined') {
        bl = br;
      }

      const absW = Math.abs(w);
      const absH = Math.abs(h);
      const hw = absW / 2;
      const hh = absH / 2;

      if (absW < 2 * tl) {
        tl = hw;
      }
      if (absH < 2 * tl) {
        tl = hh;
      }
      if (absW < 2 * tr) {
        tr = hw;
      }
      if (absH < 2 * tr) {
        tr = hh;
      }
      if (absW < 2 * br) {
        br = hw;
      }
      if (absH < 2 * br) {
        br = hh;
      }
      if (absW < 2 * bl) {
        bl = hw;
      }
      if (absH < 2 * bl) {
        bl = hh;
      }

      this.path.roundRect(x, y, w, h, [tl, tr, br, bl]);
    }
  }
  visitQuadStrip(quadStrip) {
    for (let i = 0; i < quadStrip.vertices.length - 3; i += 2) {
      const v0 = quadStrip.vertices[i];
      const v1 = quadStrip.vertices[i + 1];
      const v2 = quadStrip.vertices[i + 2];
      const v3 = quadStrip.vertices[i + 3];
      this.path.moveTo(v0.position.x, v0.position.y);
      this.path.lineTo(v1.position.x, v1.position.y);
      // These are intentionally out of order to go around the quad
      this.path.lineTo(v3.position.x, v3.position.y);
      this.path.lineTo(v2.position.x, v2.position.y);
      this.path.closePath();
    }
  }
}

class PrimitiveToVerticesConverter extends PrimitiveVisitor {
  contours = [];
  curveDetail;
  pointsToLines;

  constructor({ curveDetail = 1, pointsToLines = true } = {}) {
    super();
    this.curveDetail = curveDetail;
    this.pointsToLines = pointsToLines;
  }

  lastContour() {
    return this.contours[this.contours.length - 1];
  }

  visitAnchor(anchor) {
    this.contours.push([]);
    // Weird edge case: if the next segment is a spline, we might
    // need to jump to a different vertex.
    const next = anchor._nextPrimitive;
    if (next?.canOverrideAnchor) {
      this.lastContour().push(next._firstInterpolatedVertex);
    } else {
      this.lastContour().push(anchor.getEndVertex());
    }
  }
  visitLineSegment(lineSegment) {
    const contour = this.lastContour();
    const vertices = lineSegment.vertices;
    for (let i = 0; i < vertices.length; i++) {
      contour.push(vertices[i]);
    }
  }
  visitBezierSegment(bezierSegment) {
    const contour = this.lastContour();
    const numPoints = Math.max(
      1,
      Math.ceil(bezierSegment.hullLength() * this.curveDetail)
    );
    const vertexArrays = [
      bezierSegment.getStartVertex(),
      ...bezierSegment.vertices
    ].map(v => bezierSegment._shape.vertexToArray(v));
    for (let i = 0; i < numPoints; i++) {
      const t = (i + 1) / numPoints;
      contour.push(
        bezierSegment._shape.arrayToVertex(
          bezierSegment.order === 3
            ? bezierSegment._shape.evaluateCubicBezier(vertexArrays, t)
            : bezierSegment._shape.evaluateQuadraticBezier(vertexArrays, t)
        )
      );
    }
  }
  visitSplineSegment(splineSegment) {
    const shape = splineSegment._shape;
    const contour = this.lastContour();

    const arrayVertices = splineSegment.getControlPoints().map(
      v => shape.vertexToArray(v)
    );
    let bezierArrays = shape.catmullRomToBezier(
      arrayVertices,
      splineSegment._splineProperties.tightness
    );
    let startVertex = shape.vertexToArray(
      splineSegment._firstInterpolatedVertex
    );
    for (const array of bezierArrays) {
      const bezierControls = [startVertex, ...array];
      const numPoints = Math.max(
        1,
        Math.ceil(
          polylineLength(bezierControls.map(v => shape.arrayToVertex(v))) *
          this.curveDetail
        )
      );
      for (let i = 0; i < numPoints; i++) {
        const t = (i + 1) / numPoints;
        contour.push(
          shape.arrayToVertex(shape.evaluateCubicBezier(bezierControls, t))
        );
      }
      startVertex = array[2];
    }
  }
  visitPoint(point) {
    if (this.pointsToLines) {
      this.contours.push(...point.vertices.map(v => [v, v]));
    } else {
      this.contours.push(point.vertices.slice());
    }
  }
  visitLine(line) {
    this.contours.push(line.vertices.slice());
  }
  visitTriangle(triangle) {
    this.contours.push(triangle.vertices.slice());
  }
  visitQuad(quad) {
    this.contours.push(quad.vertices.slice());
  }
  visitTriangleFan(triangleFan) {
    // WebGL itself interprets the vertices as a fan, no reformatting needed
    this.contours.push(triangleFan.vertices.slice());
  }
  visitTriangleStrip(triangleStrip) {
    // WebGL itself interprets the vertices as a strip, no reformatting needed
    this.contours.push(triangleStrip.vertices.slice());
  }
  visitQuadStrip(quadStrip) {
    // WebGL itself interprets the vertices as a strip, no reformatting needed
    this.contours.push(quadStrip.vertices.slice());
  }
  visitArcPrimitive(arc) {
    const startVertex = arc.startVertex;
    const endVertex = arc.endVertex;
    const centerX = arc.x + arc.w / 2;
    const centerY = arc.y + arc.h / 2;
    const radiusX = arc.w / 2;
    const radiusY = arc.h / 2;
    const avgRadius = (radiusX + radiusY) / 2;

    const arcLength = avgRadius * Math.abs(arc.stop - arc.start);

    const numPoints = Math.max(3, Math.ceil(this.curveDetail * arcLength));
    const verts = [];
    const interpolateVertexProps = (v1, v2, t) => {
    const props = {};
    for (const [key, value] of Object.entries(v1)) {
      if (key === 'position') continue;
      if (typeof value === 'number' && typeof v2[key] === 'number') {
        props[key] = value * (1 - t) + v2[key] * t;
      } else {
        props[key] = value;
      }
    }
    return props;
  };
    if (arc.mode === constants.PIE) {
      const centerProps = interpolateVertexProps(startVertex, endVertex, 0.5);
      centerProps.position = new Vector(centerX, centerY);
      verts.push(new Vertex(centerProps));
    }

    for (let i = 0; i <= numPoints; i++) {
      const t = i / numPoints;
      const angle = arc.start + (arc.stop - arc.start) * t;
      const vertexProps = interpolateVertexProps(startVertex, endVertex, t);

      vertexProps.position = new Vector(
        centerX + radiusX * Math.cos(angle),
        centerY + radiusY * Math.sin(angle)
      );

      verts.push(new Vertex(vertexProps));
    }

    this.contours.push(verts);
  }
  visitEllipsePrimitive(ellipse) {
    const centerX = ellipse.x + ellipse.w / 2;
    const centerY = ellipse.y + ellipse.h / 2;
    const radiusX = ellipse.w / 2;
    const radiusY = ellipse.h / 2;
    const avgRadius = (radiusX + radiusY) / 2;
    const perimeter = 2 * Math.PI * avgRadius;
    const numPoints = Math.max(3, Math.ceil(this.curveDetail * perimeter));
    const verts = [];
    const centerVertex = ellipse.vertices[0];
    for (let i = 0; i <= numPoints; i++) {
      const angle = (2 * Math.PI * i) / numPoints;
      const vertexProps = {};
      for (const [key, value] of Object.entries(centerVertex)) {
        if (key === 'position') continue;
        vertexProps[key] = value;
      }
      vertexProps.position = new Vector(
        centerX + radiusX * Math.cos(angle),
        centerY + radiusY * Math.sin(angle)
      );
      verts.push(new Vertex(vertexProps));
    }

    this.contours.push(verts);
  }
  visitRectPrimitive(rect) {
    const x = rect.x;
    const y = rect.y;
    const w = rect.w;
    const h = rect.h;
    let tl = rect.tl;
    let tr = rect.tr;
    let br = rect.br;
    let bl = rect.bl;

    const startVertex = rect.vertices[0];
    const getVertexProps = (px, py) => {
      const props = {};
      for (const [key, value] of Object.entries(startVertex)) {
        if (key === 'position') continue;
        props[key] = value;
      }
      props.position = new Vector(px, py);
      return new Vertex(props);
    };

    const verts = [];
    if (typeof tl === 'undefined') {
      verts.push(getVertexProps(x, y));
      verts.push(getVertexProps(x + w, y));
      verts.push(getVertexProps(x + w, y + h));
      verts.push(getVertexProps(x, y + h));
      verts.push(getVertexProps(x, y));
    } else {
      if (typeof tr === 'undefined') tr = tl;
      if (typeof br === 'undefined') br = tr;
      if (typeof bl === 'undefined') bl = br;

      const absW = Math.abs(w);
      const absH = Math.abs(h);
      const hw = absW / 2;
      const hh = absH / 2;

      if (absW < 2 * tl) tl = hw;
      if (absH < 2 * tl) tl = hh;
      if (absW < 2 * tr) tr = hw;
      if (absH < 2 * tr) tr = hh;
      if (absW < 2 * br) br = hw;
      if (absH < 2 * br) br = hh;
      if (absW < 2 * bl) bl = hw;
      if (absH < 2 * bl) bl = hh;

      const addCornerArc = (cx, cy, rx, ry, startAngle, endAngle) => {
        const perimeter = Math.PI / 2 * (rx + ry) / 2;
        const numPoints = Math.max(1, Math.ceil(this.curveDetail * perimeter));
        for (let i = 0; i <= numPoints; i++) {
          const angle = startAngle + (endAngle - startAngle) * (i / numPoints);
          verts.push(getVertexProps(cx + rx * Math.cos(angle), cy + ry * Math.sin(angle)));
        }
      };

      addCornerArc(x + tl, y + tl, tl, tl, Math.PI, 1.5 * Math.PI);
      addCornerArc(x + w - tr, y + tr, tr, tr, 1.5 * Math.PI, 2 * Math.PI);
      addCornerArc(x + w - br, y + h - br, br, br, 0, 0.5 * Math.PI);
      addCornerArc(x + bl, y + h - bl, bl, bl, 0.5 * Math.PI, Math.PI);

      verts.push(verts[0]);
    }

    this.contours.push(verts);
  }
}

class PointAtLengthGetter extends PrimitiveVisitor {
  constructor() {
    super();
  }
}

function customShapes(p5, fn) {
  // ---- GENERAL CLASSES ----

  /**
   * @private
   * A class to describe a custom shape made with `beginShape()`/`endShape()`.
   *
   * Every `Shape` has a `kind`. The kind takes any value that
   * can be passed to <a href="#/p5/beginShape">beginShape()</a>:
   *
   * - `PATH`
   * - `POINTS`
   * - `LINES`
   * - `TRIANGLES`
   * - `QUADS`
   * - `TRIANGLE_FAN`
   * - `TRIANGLE_STRIP`
   * - `QUAD_STRIP`
   *
   * A `Shape` of any kind consists of `contours`, which can be thought of as
   * subshapes (shapes inside another shape). Each `contour` is built from
   * basic shapes called primitives, and each primitive consists of one or more vertices.
   *
   * For example, a square can be made from a single path contour with four line-segment
   * primitives. Each line segment contains a vertex that indicates its endpoint. A square
   * with a circular hole in it contains the circle in a separate contour.
   *
   * By default, each vertex only has a position, but a shape's vertices may have other
   * properties such as texture coordinates, a normal vector, a fill color, and a stroke color.
   * The properties every vertex should have may be customized by passing `vertexProperties` to
   * `createShape()`.
   *
   * Once a shape is created and given a name like `myShape`, it can be built up with
   * methods such as `myShape.beginShape()`, `myShape.vertex()`, and `myShape.endShape()`.
   *
   * Vertex functions such as `vertex()` or `bezierVertex()` are used to set the `position`
   * property of vertices, as well as the `textureCoordinates` property if applicable. Those
   * properties only apply to a single vertex.
   *
   * If `vertexProperties` includes other properties, they are each set by a method of the
   * same name. For example, if vertices in `myShape` have a `fill`, then that is set with
   * `myShape.fill()`. In the same way that a <a href="#/p5/fill">fill()</a> may be applied
   * to one or more shapes, `myShape.fill()` may be applied to one or more vertices.
   *
   * @class p5.Shape
   * @param {Object} [vertexProperties={position: createVector(0, 0)}] vertex properties and their initial values.
   */

  p5.Shape = Shape;

  /**
   * @private
   * A class to describe a contour made with `beginContour()`/`endContour()`.
   *
   * Contours may be thought of as shapes inside of other shapes.
   * For example, a contour may be used to create a hole in a shape that is created
   * with <a href="#/p5/beginShape">beginShape()</a>/<a href="#/p5/endShape">endShape()</a>.
   * Multiple contours may be included inside a single shape.
   *
   * Contours can have any `kind` that a shape can have:
   *
   * - `PATH`
   * - `POINTS`
   * - `LINES`
   * - `TRIANGLES`
   * - `QUADS`
   * - `TRIANGLE_FAN`
   * - `TRIANGLE_STRIP`
   * - `QUAD_STRIP`
   *
   * By default, a contour has the same kind as the shape that contains it, but this
   * may be changed by passing a different `kind` to <a href="#/p5/beginContour">beginContour()</a>.
   *
   * A `Contour` of any kind consists of `primitives`, which are the most basic
   * shapes that can be drawn. For example, if a contour is a hexagon, then
   * it's made from six line-segment primitives.
   *
   * @class p5.Contour
   */

  p5.Contour = Contour;

  /**
   * @private
   * A base class to describe a shape primitive (a basic shape drawn with
   * `beginShape()`/`endShape()`).
   *
   * Shape primitives are the most basic shapes that can be drawn with
   * <a href="#/p5/beginShape">beginShape()</a>/<a href="#/p5/endShape">endShape()</a>:
   *
   * - segment primitives: line segments, bezier segments, spline segments, and arc segments
   * - isolated primitives: points, lines, triangles, and quads
   * - tessellation primitives: triangle fans, triangle strips, and quad strips
   *
   * More complex shapes may be created by combining many primitives, possibly of different kinds,
   * into a single shape.
   *
   * In a similar way, every shape primitive is built from one or more vertices.
   * For example, a point consists of a single vertex, while a triangle consists of three vertices.
   * Each type of shape primitive has a `vertexCapacity`, which may be `Infinity` (for example, a
   * spline may consist of any number of vertices). A primitive's `vertexCount` is the number of
   * vertices it currently contains.
   *
   * Each primitive can add itself to a shape with an `addToShape()` method.
   *
   * It can also accept visitor objects with an `accept()` method. When a primitive accepts a visitor,
   * it gives the visitor access to its vertex data. For example, one visitor to a segment might turn
   * the data into 2D drawing instructions. Another might find a point at a given distance
   * along the segment.
   *
   * @class p5.ShapePrimitive
   * @abstract
   */

  p5.ShapePrimitive = ShapePrimitive;

  /**
   * @private
   * A class to describe a vertex (a point on a shape), in 2D or 3D.
   *
   * Vertices are the basic building blocks of all `p5.Shape` objects, including
   * shapes made with <a href="#/p5/vertex">vertex()</a>, <a href="#/p5/arcVertex">arcVertex()</a>,
   * <a href="#/p5/bezierVertex">bezierVertex()</a>, and <a href="#/p5/splineVertex">splineVertex()</a>.
   *
   * Like a point on an object in the real world, a vertex may have different properties.
   * These may include coordinate properties such as `position`, `textureCoordinates`, and `normal`,
   * color properties such as `fill` and `stroke`, and more.
   *
   * A vertex called `myVertex` with position coordinates `(2, 3, 5)` and a green stroke may be created
   * like this:
   *
   * ```js
   * let myVertex = new p5.Vertex({
   *   position: createVector(2, 3, 5),
   *   stroke: color('green')
   * });
   * ```
   *
   * Any property names may be used. The `p5.Shape` class assumes that if a vertex has a
   * position or texture coordinates, they are stored in `position` and `textureCoordinates`
   * properties.
   *
   * Property values may be any
   * <a href="https://developer.mozilla.org/en-US/docs/Glossary/Primitive">JavaScript primitive</a>, any
   * <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer">object literal</a>,
   * or any object with an `array` property.
   *
   * For example, if a position is stored as a `p5.Vector` object and a stroke is stored as a `p5.Color` object,
   * then the `array` properties of those objects will be used by the vertex's own `array` property, which provides
   * all the vertex data in a single array.
   *
   * @class p5.Vertex
   * @param {Object} [properties={position: createVector(0, 0)}] vertex properties.
   */

  p5.Vertex = Vertex;

  // ---- PATH PRIMITIVES ----

  /**
   * @private
   * A class responsible for...
   *
   * @class p5.Anchor
   * @extends p5.ShapePrimitive
   * @param {p5.Vertex} vertex the vertex to include in the anchor.
   */

  p5.Anchor = Anchor;

  /**
   * @private
   * A class responsible for...
   *
   * Note: When a segment is added to a shape, it's attached to an anchor or another segment.
   * Adding it to another shape may result in unexpected behavior.
   *
   * @class p5.Segment
   * @extends p5.ShapePrimitive
   * @param {...p5.Vertex} vertices the vertices to include in the segment.
   */

  p5.Segment = Segment;

  /**
   * @private
   * A class responsible for...
   *
   * @class p5.LineSegment
   * @param {p5.Vertex} vertex the vertex to include in the anchor.
   */

  p5.LineSegment = LineSegment;

  /**
   * @private
   * A class responsible for...
   */

  p5.BezierSegment = BezierSegment;

  /**
   * @private
   * A class responsible for...
   */

  p5.SplineSegment = SplineSegment;

  // ---- ISOLATED PRIMITIVES ----

  /**
   * @private
   * A class responsible for...
   */

  p5.Point = Point;

  /**
   * @private
   * A class responsible for...
   *
   * @class p5.Line
   * @param {...p5.Vertex} vertices the vertices to include in the line.
   */

  p5.Line = Line;

  /**
   * @private
   * A class responsible for...
   */

  p5.Triangle = Triangle;

  /**
   * @private
   * A class responsible for...
   */

  p5.Quad = Quad;

  // ---- TESSELLATION PRIMITIVES ----

  /**
   * @private
   * A class responsible for...
   */

  p5.TriangleFan = TriangleFan;

  /**
   * @private
   * A class responsible for...
   */

  p5.TriangleStrip = TriangleStrip;

  /**
   * @private
   * A class responsible for...
   */

  p5.QuadStrip = QuadStrip;

  // ---- PRIMITIVE VISITORS ----

  /**
   * @private
   * A class responsible for...
   */

  p5.PrimitiveVisitor = PrimitiveVisitor;

  /**
   * @private
   * A class responsible for...
   *
   * Notes:
   * 1. Assumes vertex positions are stored as p5.Vector instances.
   * 2. Currently only supports position properties of vectors.
   */

  p5.PrimitiveToPath2DConverter = PrimitiveToPath2DConverter;

  /**
   * @private
   * A class responsible for...
   */

  p5.PrimitiveToVerticesConverter = PrimitiveToVerticesConverter;

  /**
   * @private
   * A class responsible for...
   */

  p5.PointAtLengthGetter = PointAtLengthGetter;

  // ---- FUNCTIONS ----


  /**
   * Influences the shape of the Bézier curve segment in a custom shape.
   * By default, this is 3; the other possible parameter is 2. This
   * results in quadratic Bézier curves.
   *
   * `bezierVertex()` adds a curved segment to custom shapes. The Bézier curves
   * it creates are defined like those made by the
   * <a href="#/p5/bezier">bezier()</a> function. `bezierVertex()` must be
   * called between the
   * <a href="#/p5/beginShape">beginShape()</a> and
   * <a href="#/p5/endShape">endShape()</a> functions. There must be at least
   * one call to <a href="#/p5/vertex">bezierVertex()</a>, before
   * a number of `bezierVertex()` calls that is a multiple of the parameter
   * set by <a href="#/p5/bezierOrder">bezierOrder(...)</a> (default 3).
   *
   * Each curve of order 3 requires three calls to `bezierVertex`, so
   * 2 curves would need 7 calls to `bezierVertex()`:
   * (1 one initial anchor point, two sets of 3 curves describing the curves)
   * With `bezierOrder(2)`, two curves would need 5 calls: 1 + 2 + 2.
   *
   * Bézier curves can also be drawn in 3D using WebGL mode.
   *
   * Note: `bezierVertex()` won’t work when an argument is passed to
   * <a href="#/p5/beginShape">beginShape()</a>.
   *
   * Calling `bezierOrder()` without an argument returns the current Bézier order.
   *
   * @method bezierOrder
   * @param {Number} order The new order to set. Can be either 2 or 3, by default 3
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Style the shape.
   *   noFill();
   *
   *   // Start drawing the shape.
   *   beginShape();
   *
   *   // set the order to 2 for a quadratic Bézier curve
   *   bezierOrder(2);
   *
   *   // Add the first anchor point.
   *   bezierVertex(30, 20);
   *
   *   // Add the Bézier vertex.
   *   bezierVertex(80, 20);
   *   bezierVertex(50, 50);
   *
   *   // Stop drawing the shape.
   *   endShape();
   *
   *   describe('A black curve drawn on a gray square. The curve starts at the top-left corner and ends at the center.');
   * }
   */
  /**
   * @method bezierOrder
   * @returns {Number} The current Bézier order.
   */
  fn.bezierOrder = function(order) {
    return this._renderer.bezierOrder(order);
  };



  /**
   * Connects points with a smooth curve (a spline).
   *
   * `splineVertex()` adds a curved segment to custom shapes.
   * The curve it creates follows the same rules as the ones
   * made with the <a href="#/p5/spline">spline()</a> function.
   * `splineVertex()` must be called between the
   * <a href="#/p5/beginShape">beginShape()</a> and
   * <a href="#/p5/endShape">endShape()</a> functions.
   *
   * Spline curves can form shapes and curves that slope gently. They’re like
   * cables that are attached to a set of points. `splineVertex()` draws a smooth
   * curve through the points you give it.
   * <a href="#/p5/beginShape">beginShape()</a> and
   * <a href="#/p5/endShape">endShape()</a> in order to draw a curve:
   *
   *
   * If you provide three points, the spline will pass through them.
   * It works the same way with any number of points.
   *
   *
   *
   * ```js
   * beginShape();
   *
   * // Add the first point.
   * splineVertex(25, 80);
   *
   * // Add the second point.
   * splineVertex(20, 30);
   *
   * // Add the last point.
   * splineVertex(85, 60);
   *
   * endShape();
   * ```
   *
   * <img src="assets/openCurveSpline.png"></img>
   *
   *
   * Passing in `CLOSE` to `endShape()` closes the spline smoothly.
   * ```js
   * beginShape();
   *
   * // Add the first point.
   * splineVertex(25, 80);
   *
   * // Add the second point.
   * splineVertex(20, 30);
   *
   * // Add the second point.
   * splineVertex(85, 60);
   *
   * endShape(CLOSE);
   * ```
   *
   * <img src="assets/closeCurveSpline.png"></img>
   *
   *
   * By default (`ends: INCLUDE`), the curve passes through
   * all the points you add with `splineVertex()`, similar to
   * the <a href="#/p5/spline">spline()</a> function. To draw only
   * the middle span p1->p2 (skipping p0->p1 and p2->p3), set
   * `splineProperty('ends', EXCLUDE)`. You don’t need to duplicate
   * vertices to draw those spans.
   *
   * Spline curves can also be drawn in 3D using WebGL mode. The 3D version of
   * `splineVertex()` has three arguments because each point has x-, y-, and
   * z-coordinates. By default, the vertex’s z-coordinate is set to 0.
   *
   * Note: `splineVertex()` won’t work when an argument is passed to
   * <a href="#/p5/beginShape">beginShape()</a>.
   *
   * @method splineVertex
   * @param {Number} x x-coordinate of the vertex
   * @param {Number} y y-coordinate of the vertex
   * @chainable
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   background(220);
   *   noFill();
   *   strokeWeight(1);
   *
   *   beginShape();
   *   splineVertex(25, 80);
   *   splineVertex(20, 30);
   *   splineVertex(85, 60);
   *   endShape();
   *
   *   strokeWeight(5);
   *   stroke(0);
   *
   *   point(25, 80);
   *   point(20, 30);
   *   point(85, 60);
   *
   *   describe(
   *     'On a gray background, a black spline passes through three marked points.'
   *   );
   * }
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *   background(220);
   *
   *   beginShape();
   *   splineVertex(25, 80);
   *   splineVertex(20, 30);
   *   splineVertex(85, 60);
   *   endShape(CLOSE);
   *
   *   describe(
   *     'On a gray background, a closed black spline with a white interior forms a triangular shape with smooth corners.'
   *   );
   * }
   *
   * @example
   * let ringInnerRadius, ringWidth;
   * let radius, dRadius;
   * let theta, dTheta;
   * let time, dTime;
   * let vertexCount, unit, offset;
   *
   * function setup() {
   *   createCanvas(400, 400);
   *
   *   vertexCount = 15;
   *   unit = createVector(1, 0);
   *   dTheta = TAU / vertexCount;
   *   dTime = 0.004;
   *
   *   ringInnerRadius = 25;
   *   ringWidth = 5 * ringInnerRadius;
   *
   *   offset = width;
   *
   *   describe(
   *     'A white blob with a black outline changes its shape over time.'
   *   );
   * }
   *
   * function draw() {
   *   background(220);
   *   strokeWeight(2);
   *   translate(width / 2, height / 2);
   *
   *   time = dTime * frameCount;
   *
   *   beginShape();
   *   for (let i = 0; i < vertexCount; i++) {
   *     unit.rotate(dTheta);
   *     dRadius = noise(offset + unit.x, offset + unit.y, time) * ringWidth;
   *     radius = ringInnerRadius + dRadius;
   *     splineVertex(radius * unit.x, radius * unit.y);
   *   }
   *   endShape(CLOSE);
   * }
   *
   * @example
   * let vertexA;
   * let vertexB;
   * let vertexC;
   * let vertexD;
   * let vertexE;
   * let vertexF;
   *
   * let markerRadius;
   *
   * let vectorAB;
   * let vectorFE;
   *
   * let endOfTangentB;
   * let endOfTangentE;
   *
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   // Initialize variables
   *   // Adjusting vertices A and F affects the slopes at B and E
   *
   *   vertexA = createVector(35, 85);
   *   vertexB = createVector(25, 70);
   *   vertexC = createVector(30, 30);
   *   vertexD = createVector(70, 30);
   *   vertexE = createVector(75, 70);
   *   vertexF = createVector(65, 85);
   *
   *   markerRadius = 4;
   *
   *   vectorAB = p5.Vector.sub(vertexB, vertexA);
   *   vectorFE = p5.Vector.sub(vertexE, vertexF);
   *
   *   endOfTangentB = p5.Vector.add(vertexC, vectorAB);
   *   endOfTangentE = p5.Vector.add(vertexD, vectorFE);
   *
   *   splineProperty(`ends`, EXCLUDE);
   *
   *   // Draw figure
   *
   *   background(220);
   *
   *   noFill();
   *
   *   beginShape();
   *   splineVertex(vertexA.x, vertexA.y);
   *   splineVertex(vertexB.x, vertexB.y);
   *   splineVertex(vertexC.x, vertexC.y);
   *   splineVertex(vertexD.x, vertexD.y);
   *   splineVertex(vertexE.x, vertexE.y);
   *   splineVertex(vertexF.x, vertexF.y);
   *   endShape();
   *
   *   stroke('red');
   *   line(vertexA.x, vertexA.y, vertexC.x, vertexC.y);
   *   line(vertexB.x, vertexB.y, endOfTangentB.x, endOfTangentB.y);
   *
   *   stroke('blue');
   *   line(vertexD.x, vertexD.y, vertexF.x, vertexF.y);
   *   line(vertexE.x, vertexE.y, endOfTangentE.x, endOfTangentE.y);
   *
   *   fill('white');
   *   stroke('black');
   *   circle(vertexA.x, vertexA.y, markerRadius);
   *   circle(vertexB.x, vertexB.y, markerRadius);
   *   circle(vertexC.x, vertexC.y, markerRadius);
   *   circle(vertexD.x, vertexD.y, markerRadius);
   *   circle(vertexE.x, vertexE.y, markerRadius);
   *   circle(vertexF.x, vertexF.y, markerRadius);
   *
   *   fill('black');
   *   noStroke();
   *   text('A', vertexA.x - 15, vertexA.y + 5);
   *   text('B', vertexB.x - 15, vertexB.y + 5);
   *   text('C', vertexC.x - 5, vertexC.y - 5);
   *   text('D', vertexD.x - 5, vertexD.y - 5);
   *   text('E', vertexE.x + 5, vertexE.y + 5);
   *   text('F', vertexF.x + 5, vertexF.y + 5);
   *
   *   describe('On a gray background, a black spline passes through vertices A, B, C, D, E, and F, shown as white circles. A red line segment joining vertices A and C has the same slope as the red tangent segment at B. Similarly, the blue line segment joining vertices D and F has the same slope as the blue tangent at E.');
   * }
   */
  /**
   * @method splineVertex
   * @param {Number} x
   * @param {Number} y
   * @param {Number} [z] z-coordinate of the vertex.
   * @chainable
   *
   * @example
   * // Click and drag the mouse to view the scene from different angles.
   *
   * function setup() {
   *   createCanvas(100, 100, WEBGL);
   *
   *   describe('A ghost shape drawn in white on a blue background. When the user drags the mouse, the scene rotates to reveal the outline of a second ghost.');
   * }
   *
   * function draw() {
   *   background('midnightblue');
   *
   *   // Enable orbiting with the mouse.
   *   orbitControl();
   *
   *   // Draw the first ghost.
   *   noStroke();
   *   fill('ghostwhite');
   *
   *   beginShape();
   *   splineVertex(-28, 41, 0);
   *   splineVertex(-28, 41, 0);
   *   splineVertex(-29, -33, 0);
   *   splineVertex(18, -31, 0);
   *   splineVertex(34, 41, 0);
   *   splineVertex(34, 41, 0);
   *   endShape();
   *
   *   // Draw the second ghost.
   *   noFill();
   *   stroke('ghostwhite');
   *
   *   beginShape();
   *   splineVertex(-28, 41, -20);
   *   splineVertex(-28, 41, -20);
   *   splineVertex(-29, -33, -20);
   *   splineVertex(18, -31, -20);
   *   splineVertex(34, 41, -20);
   *   splineVertex(34, 41, -20);
   *   endShape();
   * }
   */
  /**
   * @method splineVertex
   * @param {Number} x
   * @param {Number} y
   * @param {Number} [u=0]
   * @param {Number} [v=0]
   */
  /**
   * @method splineVertex
   * @param {Number} x
   * @param {Number} y
   * @param {Number} z
   * @param {Number} [u=0]
   * @param {Number} [v=0]
   */
  fn.splineVertex = function(...args) {
    let x = 0, y = 0, z = 0, u = 0, v = 0;
    if (args.length === 2) {
      [x, y] = args;
    } else if (args.length === 4) {
      [x, y, u, v] = args;
    } else if (args.length === 3) {
      [x, y, z] = args;
    } else if (args.length === 5) {
      [x, y, z, u, v] = args;
    }
    this._renderer.splineVertex(x, y, z, u, v);
  };

  /**
   * Gets or sets a given spline property.
   *
   * Use `splineProperty()` to adjust the behavior of splines
   * created with `splineVertex()` or `spline()`. You can control
   * two key aspects of a spline: its end behavior (`ends`) and
   * its curvature (`tightness`).
   *
   * By default, the ends property is set to `INCLUDE`, which means
   * the spline passes through every point, including the endpoints.
   * You can also set it to `EXCLUDE` i.e. `splineProperty('ends', EXCLUDE)`,
   * which makes the spline pass through all points except the endpoints.
   *
   * `INCLUDE` case will have the spline passing through
   * all points, like this:
   *
   * ```js
   * splineProperty('ends', INCLUDE); // no need to set this, as it is the default
   * spline(25, 46, 93, 44, 93, 81, 35, 85);
   *
   * point(25, 46);
   * point(93, 44);
   * point(93, 81);
   * point(35, 85);
   * ```
   *
   * <img src="assets/includeSpline.png"></img>
   *
   *
   * EXCLUDE case will have the spline passing through
   * the middle points, like this:
   *
   *
   * ```js
   * splineProperty('ends', EXCLUDE);
   * spline(25, 46, 93, 44, 93, 81, 35, 85);
   *
   * point(25, 46);
   * point(93, 44);
   * point(93, 81);
   * point(35, 85);
   * ```
   *
   * <img src="assets/excludeSpline.png"></img>
   *
   * By default, the tightness property is set to `0`,
   * producing a smooth curve that passes evenly through
   * the vertices. Negative values make the curve looser,
   * while positive values make it tighter. Common values
   * range between -1 and 1, though values outside this
   * range can also be used for different effects.
   *
   * For example, To set tightness, use `splineProperty('tightness', t)`,
   * (default: t = 0).
   *
   * Here's the example showing negetive value of tightness,
   * which creates a rounder bulge:
   *
   * ```js
   * splineProperty('tightness', -5)
   * stroke(0);
   * strokeWeight(2);
   * spline(25, 46, 93, 44, 93, 81, 35, 85);
   * ```
   * <img src="assets/roundBulge.png"></img>
   * Here's the example showing positive value of tightness,
   * which makes the curve tighter and more angular:
   *
   * ```js
   * splineProperty('tightness', 5)
   * stroke(0);
   * strokeWeight(2);
   * spline(25, 46, 93, 44, 93, 81, 35, 85);
   * ```
   * <img src="assets/anglurBulge.png"></img>
   *
   * In all cases, the splines in p5.js are <a href = "https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Cardinal_spline">cardinal splines</a>.
   * When tightness is 0, these splines are often known as
   * <a href="https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Catmull%E2%80%93Rom_spline">Catmull-Rom splines</a>
   *
   * @method splineProperty
   * @param {String} property
   * @param value Value to set the given property to.
   *
   * @example
   * // Move the mouse left and right to see the curve change.
   *
   * let t;
   *
   * function setup() {
   *   createCanvas(100, 100);
   * }
   *
   * function draw() {
   *   background(240);
   *
   *   t = map(mouseX, 0, width, -5, 5, true);
   *   splineProperty('tightness', t);
   *
   *   noFill();
   *   stroke(0);
   *   strokeWeight(2);
   *
   *   beginShape();
   *   splineVertex(10, 26);
   *   splineVertex(83, 24);
   *
   *   splineVertex(83, 61);
   *   splineVertex(25, 65);
   *   endShape();
   *
   *   push();
   *   strokeWeight(5);
   *   point(10, 26);
   *   point(83, 24);
   *   point(83, 61);
   *   point(25, 65);
   *   pop();
   *
   *   fill(0);
   *   noStroke();
   *   textSize(10);
   *   text(`tightness: ${round(t, 1)}`, 15, 90);
   *   describe('A black spline forms a sideways U shape through four points. The spline passes through the points more loosely as the mouse moves left of center (negative tightness), and more tightly as it moves right of center (positive tightness). The tightness is displayed at the bottom.');
   * }
   *
   * @example
   * function setup() {
   *   createCanvas(360, 140);
   *   background(240);
   *   noFill();
   *
   *   // Right panel: ends = INCLUDE (all spans).
   *   push();
   *   translate(10, 10);
   *   stroke(220);
   *   rect(0, 0, 160, 120);
   *   fill(30);
   *   textSize(11);
   *   text('ends: INCLUDE (all spans)', 8, 16);
   *   noFill();
   *
   *   splineProperty('ends', INCLUDE);
   *   stroke(0);
   *   strokeWeight(2);
   *   spline(25, 46, 93, 44, 93, 81, 35, 85);
   *
   *   // vertices
   *   strokeWeight(5);
   *   stroke(0);
   *   point(25, 46);
   *   point(93, 44);
   *   point(93, 81);
   *   point(35, 85);
   *   pop();
   *
   *   // Right panel: ends = EXCLUDE (middle only).
   *   push();
   *   translate(190, 10);
   *   stroke(220);
   *   rect(0, 0, 160, 120);
   *   noStroke();
   *   fill(30);
   *   text('ends: EXCLUDE ', 18, 16);
   *   noFill();
   *
   *   splineProperty('ends', EXCLUDE);
   *   stroke(0);
   *   strokeWeight(2);
   *   spline(25, 46, 93, 44, 93, 81, 35, 85);
   *
   *   // vertices
   *   strokeWeight(5);
   *   stroke(0);
   *   point(25, 46);
   *   point(93, 44);
   *   point(93, 81);
   *   point(35, 85);
   *   pop();
   *
   *   describe('Left panel shows spline with ends INCLUDE (three spans). Right panel shows EXCLUDE (only the middle span). Four black points mark the vertices.');
   * }
   *
   * @example
   * let vertexA;
   * let vertexB;
   * let vertexC;
   * let vertexD;
   * let vertexE;
   * let vertexF;
   *
   * let markerRadius;
   *
   * let vectorAB;
   * let vectorFE;
   *
   * let endOfTangentB;
   * let endOfTangentE;
   *
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   // Initialize variables
   *   // Adjusting vertices A and F affects the slopes at B and E
   *
   *   vertexA = createVector(35, 85);
   *   vertexB = createVector(25, 70);
   *   vertexC = createVector(30, 30);
   *   vertexD = createVector(70, 30);
   *   vertexE = createVector(75, 70);
   *   vertexF = createVector(65, 85);
   *
   *   markerRadius = 4;
   *
   *   vectorAB = p5.Vector.sub(vertexB, vertexA);
   *   vectorFE = p5.Vector.sub(vertexE, vertexF);
   *
   *   endOfTangentB = p5.Vector.add(vertexC, vectorAB);
   *   endOfTangentE = p5.Vector.add(vertexD, vectorFE);
   *
   *   splineProperty(`ends`, EXCLUDE);
   *
   *   // Draw figure
   *
   *   background(220);
   *
   *   noFill();
   *
   *   beginShape();
   *   splineVertex(vertexA.x, vertexA.y);
   *   splineVertex(vertexB.x, vertexB.y);
   *   splineVertex(vertexC.x, vertexC.y);
   *   splineVertex(vertexD.x, vertexD.y);
   *   splineVertex(vertexE.x, vertexE.y);
   *   splineVertex(vertexF.x, vertexF.y);
   *   endShape();
   *
   *   stroke('red');
   *   line(vertexA.x, vertexA.y, vertexC.x, vertexC.y);
   *   line(vertexB.x, vertexB.y, endOfTangentB.x, endOfTangentB.y);
   *
   *   stroke('blue');
   *   line(vertexD.x, vertexD.y, vertexF.x, vertexF.y);
   *   line(vertexE.x, vertexE.y, endOfTangentE.x, endOfTangentE.y);
   *
   *   fill('white');
   *   stroke('black');
   *   circle(vertexA.x, vertexA.y, markerRadius);
   *   circle(vertexB.x, vertexB.y, markerRadius);
   *   circle(vertexC.x, vertexC.y, markerRadius);
   *   circle(vertexD.x, vertexD.y, markerRadius);
   *   circle(vertexE.x, vertexE.y, markerRadius);
   *   circle(vertexF.x, vertexF.y, markerRadius);
   *
   *   fill('black');
   *   noStroke();
   *   text('A', vertexA.x - 15, vertexA.y + 5);
   *   text('B', vertexB.x - 15, vertexB.y + 5);
   *   text('C', vertexC.x - 5, vertexC.y - 5);
   *   text('D', vertexD.x - 5, vertexD.y - 5);
   *   text('E', vertexE.x + 5, vertexE.y + 5);
   *   text('F', vertexF.x + 5, vertexF.y + 5);
   *
   *   describe('On a gray background, a black spline passes through vertices A, B, C, D, E, and F, shown as white circles. A red line segment joining vertices A and C has the same slope as the red tangent segment at B. Similarly, the blue line segment joining vertices D and F has the same slope as the blue tangent at E.');
   * }
   */
  /**
   * @method splineProperty
   * @param {String} property
   * @returns The current value for the given property.
   */
  fn.splineProperty = function(property, value) {
    return this._renderer.splineProperty(property, value);
  };

  /**
   * Sets multiple properties for spline curves at once.
   *
   * `splineProperties()` accepts an object with key-value pairs to configure
   * how spline curves are drawn. This is a convenient way to set multiple
   * spline properties with a single function call, rather than calling
   * <a href="#/p5/splineProperty">splineProperty()</a> multiple times.
   *
   * The properties object can include:
   * - `tightness`: A number that controls how tightly the curve fits to the
   *   vertex points. The default value is 0. Positive values make the curve
   *   tighter (straighter), while negative values make it looser. Values
   *   between -5 and 5 work best.
   * - `ends`: Controls whether to draw the end segments of the spline. Set to
   *   `EXCLUDE` to skip drawing the segments between the first and second
   *   points and between the second-to-last and last points. This is useful
   *   when you want to use the first and last points as control points only.
   *
   * `splineProperties()` affects curves drawn with
   * <a href="#/p5/splineVertex">splineVertex()</a> within
   * <a href="#/p5/beginShape">beginShape()</a> and
   * <a href="#/p5/endShape">endShape()</a>, as well as curves drawn with
   * <a href="#/p5/spline">spline()</a>. The properties remain active until
   * changed by another call to `splineProperties()` or
   * <a href="#/p5/splineProperty">splineProperty()</a>.
   *
   * @method splineProperties
   * @param  {Object} values an object containing spline property key-value pairs
   * @chainable
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *   background(220);
   *
   *   // Set spline tightness using splineProperties
   *   splineProperties({
   *     tightness: 0.5
   *   });
   *
   *   // Draw a spline curve
   *   noFill();
   *   stroke(0);
   *   strokeWeight(2);
   *
   *   beginShape();
   *   splineVertex(20, 80);
   *   splineVertex(30, 30);
   *   splineVertex(70, 30);
   *   splineVertex(80, 80);
   *   endShape();
   *
   *   // Show vertex points
   *   fill(255, 0, 0);
   *   noStroke();
   *   circle(20, 80, 6);
   *   circle(30, 30, 6);
   *   circle(70, 30, 6);
   *   circle(80, 80, 6);
   *
   *   describe('A smooth curved line with tightness 0.5 connecting four red points.');
   * }
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *   background(220);
   *
   *   // Exclude end segments - first and last points become control points
   *   splineProperties({
   *     tightness: 0,
   *     ends: EXCLUDE
   *   });
   *
   *   // Draw curve only between middle points
   *   noFill();
   *   stroke(0);
   *   strokeWeight(2);
   *
   *   beginShape();
   *   splineVertex(10, 50);  // Control point (affects curve but not drawn to)
   *   splineVertex(30, 20);  // Start of visible curve
   *   splineVertex(70, 80);  // End of visible curve
   *   splineVertex(90, 50);  // Control point (affects curve but not drawn to)
   *   endShape();
   *
   *   // Show all points
   *   fill(200, 0, 0);
   *   noStroke();
   *   circle(10, 50, 6);  // Control point
   *   circle(90, 50, 6);  // Control point
   *
   *   fill(0, 0, 255);
   *   circle(30, 20, 6);  // Visible curve point
   *   circle(70, 80, 6);  // Visible curve point
   *
   *   describe('A curved line between two blue points, with red control points at the ends.');
   * }
   *
   * @method splineProperties
   * @return {Object}
   */
  fn.splineProperties = function(values) {
    return this._renderer.splineProperties(values);
  };

  /**
   * Adds a vertex to a custom shape.
   *
   * `vertex()` sets the coordinates of vertices drawn between the
   * <a href="#/p5/beginShape">beginShape()</a> and
   * <a href="#/p5/endShape">endShape()</a> functions.
   *
   * The first two parameters, `x` and `y`, set the x- and y-coordinates of the
   * vertex.
   *
   * The third parameter, `z`, is optional. It sets the z-coordinate of the
   * vertex in WebGL mode. By default, `z` is 0.
   *
   * The fourth and fifth parameters, `u` and `v`, are also optional. They set
   * the u- and v-coordinates for the vertex’s texture when used with
   * <a href="#/p5/endShape">endShape()</a>. By default, `u` and `v` are both 0.
   *
   * @method vertex
   * @param  {Number} x x-coordinate of the vertex.
   * @param  {Number} y y-coordinate of the vertex.
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Style the shape.
   *   strokeWeight(3);
   *
   *   // Start drawing the shape.
   *   // Only draw the vertices.
   *   beginShape(POINTS);
   *
   *   // Add the vertices.
   *   vertex(30, 20);
   *   vertex(85, 20);
   *   vertex(85, 75);
   *   vertex(30, 75);
   *
   *   // Stop drawing the shape.
   *   endShape();
   *
   *   describe('Four black dots that form a square are drawn on a gray background.');
   * }
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Start drawing the shape.
   *   beginShape();
   *
   *   // Add vertices.
   *   vertex(30, 20);
   *   vertex(85, 20);
   *   vertex(85, 75);
   *   vertex(30, 75);
   *
   *   // Stop drawing the shape.
   *   endShape(CLOSE);
   *
   *   describe('A white square on a gray background.');
   * }
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100, WEBGL);
   *
   *   background(200);
   *
   *   // Start drawing the shape.
   *   beginShape();
   *
   *   // Add vertices.
   *   vertex(-20, -30, 0);
   *   vertex(35, -30, 0);
   *   vertex(35, 25, 0);
   *   vertex(-20, 25, 0);
   *
   *   // Stop drawing the shape.
   *   endShape(CLOSE);
   *
   *   describe('A white square on a gray background.');
   * }
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100, WEBGL);
   *
   *   describe('A white square spins around slowly on a gray background.');
   * }
   *
   * function draw() {
   *   background(200);
   *
   *   // Rotate around the y-axis.
   *   rotateY(frameCount * 0.01);
   *
   *   // Start drawing the shape.
   *   beginShape();
   *
   *   // Add vertices.
   *   vertex(-20, -30, 0);
   *   vertex(35, -30, 0);
   *   vertex(35, 25, 0);
   *   vertex(-20, 25, 0);
   *
   *   // Stop drawing the shape.
   *   endShape(CLOSE);
   * }
   *
   * @example
   * let img;
   *
   * async function setup() {
   *   // Load an image to apply as a texture.
   *   img = await loadImage('assets/laDefense.jpg');
   *
   *   createCanvas(100, 100, WEBGL);
   *
   *   describe('A photograph of a ceiling rotates slowly against a gray background.');
   * }
   *
   * function draw() {
   *   background(200);
   *
   *   // Rotate around the y-axis.
   *   rotateY(frameCount * 0.01);
   *
   *   // Style the shape.
   *   noStroke();
   *
   *   // Apply the texture.
   *   texture(img);
   *   textureMode(NORMAL);
   *
   *   // Start drawing the shape
   *   beginShape();
   *
   *   // Add vertices.
   *   vertex(-20, -30, 0, 0, 0);
   *   vertex(35, -30, 0, 1, 0);
   *   vertex(35, 25, 0, 1, 1);
   *   vertex(-20, 25, 0, 0, 1);
   *
   *   // Stop drawing the shape.
   *   endShape();
   * }
   *
   * @example
   * let vid;
   * function setup() {
   *   // Load a video and create a p5.MediaElement object.
   *   vid = createVideo('/assets/fingers.mov');
   *   createCanvas(100, 100, WEBGL);
   *
   *   // Hide the video.
   *   vid.hide();
   *
   *   // Set the video to loop.
   *   vid.loop();
   *
   *   describe('A rectangle with video as texture');
   * }
   *
   * function draw() {
   *   background(0);
   *
   *   // Rotate around the y-axis.
   *   rotateY(frameCount * 0.01);
   *
   *   // Set the texture mode.
   *   textureMode(NORMAL);
   *
   *   // Apply the video as a texture.
   *   texture(vid);
   *
   *   // Draw a custom shape using uv coordinates.
   *   beginShape();
   *   vertex(-40, -40, 0, 0);
   *   vertex(40, -40, 1, 0);
   *   vertex(40, 40, 1, 1);
   *   vertex(-40, 40, 0, 1);
   *   endShape();
   * }
   */
  /**
   * @method vertex
   * @param  {Number} x
   * @param  {Number} y
   * @param  {Number} [u=0]   u-coordinate of the vertex's texture.
   * @param  {Number} [v=0]   v-coordinate of the vertex's texture.
   */
  /**
   * @method vertex
   * @param  {Number} x
   * @param  {Number} y
   * @param  {Number} z
   * @param  {Number} [u=0]   u-coordinate of the vertex's texture.
   * @param  {Number} [v=0]   v-coordinate of the vertex's texture.
   */
  fn.vertex = function(x, y) {
    let z, u, v;

    // default to (x, y) mode: all other arguments assumed to be 0.
    z = u = v = 0;

    if (arguments.length === 3) {
      // (x, y, z) mode: (u, v) assumed to be 0.
      z = arguments[2];
    } else if (arguments.length === 4) {
      // (x, y, u, v) mode: z assumed to be 0.
      u = arguments[2];
      v = arguments[3];
    } else if (arguments.length === 5) {
      // (x, y, z, u, v) mode
      z = arguments[2];
      u = arguments[3];
      v = arguments[4];
    }
    this._renderer.vertex(x, y, z, u, v);
    return;
  };

  /**
   * Begins creating a hole within a flat shape.
   *
   * The `beginContour()` and <a href="#/p5/endContour">endContour()</a>
   * functions allow for creating negative space within custom shapes that are
   * flat. `beginContour()` begins adding vertices to a negative space and
   * <a href="#/p5/endContour">endContour()</a> stops adding them.
   * `beginContour()` and <a href="#/p5/endContour">endContour()</a> must be
   * called between <a href="#/p5/beginShape">beginShape()</a> and
   * <a href="#/p5/endShape">endShape()</a>.
   *
   * Transformations such as <a href="#/p5/translate">translate()</a>,
   * <a href="#/p5/rotate">rotate()</a>, and <a href="#/p5/scale">scale()</a>
   * don't work between `beginContour()` and
   * <a href="#/p5/endContour">endContour()</a>. It's also not possible to use
   * other shapes, such as <a href="#/p5/ellipse">ellipse()</a> or
   * <a href="#/p5/rect">rect()</a>, between `beginContour()` and
   * <a href="#/p5/endContour">endContour()</a>.
   *
   * Note: The vertices that define a negative space must "wind" in the opposite
   * direction from the outer shape. First, draw vertices for the outer shape
   * clockwise order. Then, draw vertices for the negative space in
   * counter-clockwise order.
   *
   * @method beginContour
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Start drawing the shape.
   *   beginShape();
   *
   *   // Exterior vertices, clockwise winding.
   *   vertex(10, 10);
   *   vertex(90, 10);
   *   vertex(90, 90);
   *   vertex(10, 90);
   *
   *   // Interior vertices, counter-clockwise winding.
   *   beginContour();
   *   vertex(30, 30);
   *   vertex(30, 70);
   *   vertex(70, 70);
   *   vertex(70, 30);
   *   endContour(CLOSE);
   *
   *   // Stop drawing the shape.
   *   endShape(CLOSE);
   *
   *   describe('A white square with a square hole in its center drawn on a gray background.');
   * }
   *
   * @example
   * // Click and drag the mouse to view the scene from different angles.
   *
   * function setup() {
   *   createCanvas(100, 100, WEBGL);
   *
   *   describe('A white square with a square hole in its center drawn on a gray background.');
   * }
   *
   * function draw() {
   *   background(200);
   *
   *   // Enable orbiting with the mouse.
   *   orbitControl();
   *
   *   // Start drawing the shape.
   *   beginShape();
   *
   *   // Exterior vertices, clockwise winding.
   *   vertex(-40, -40);
   *   vertex(40, -40);
   *   vertex(40, 40);
   *   vertex(-40, 40);
   *
   *   // Interior vertices, counter-clockwise winding.
   *   beginContour();
   *   vertex(-20, -20);
   *   vertex(-20, 20);
   *   vertex(20, 20);
   *   vertex(20, -20);
   *   endContour(CLOSE);
   *
   *   // Stop drawing the shape.
   *   endShape(CLOSE);
   * }
   */
  fn.beginContour = function(kind) {
    this._renderer.beginContour(kind);
  };

  /**
   * Stops creating a hole within a flat shape.
   *
   * The <a href="#/p5/beginContour">beginContour()</a> and `endContour()`
   * functions allow for creating negative space within custom shapes that are
   * flat. <a href="#/p5/beginContour">beginContour()</a> begins adding vertices
   * to a negative space and `endContour()` stops adding them.
   * <a href="#/p5/beginContour">beginContour()</a> and `endContour()` must be
   * called between <a href="#/p5/beginShape">beginShape()</a> and
   * <a href="#/p5/endShape">endShape()</a>.
   *
   *  By default,
   * the controur has an `OPEN` end, and to close it,
   * call `endContour(CLOSE)`. The CLOSE contour mode closes splines smoothly.
   *
   * Transformations such as <a href="#/p5/translate">translate()</a>,
   * <a href="#/p5/rotate">rotate()</a>, and <a href="#/p5/scale">scale()</a>
   * don't work between <a href="#/p5/beginContour">beginContour()</a> and
   * `endContour()`. It's also not possible to use other shapes, such as
   * <a href="#/p5/ellipse">ellipse()</a> or <a href="#/p5/rect">rect()</a>,
   * between <a href="#/p5/beginContour">beginContour()</a> and `endContour()`.
   *
   * Note: The vertices that define a negative space must "wind" in the opposite
   * direction from the outer shape. First, draw vertices for the outer shape
   * clockwise order. Then, draw vertices for the negative space in
   * counter-clockwise order.
   *
   * @method endContour
   * @param {OPEN|CLOSE} [mode=OPEN] By default, the value is OPEN
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Start drawing the shape.
   *   beginShape();
   *
   *   // Exterior vertices, clockwise winding.
   *   vertex(10, 10);
   *   vertex(90, 10);
   *   vertex(90, 90);
   *   vertex(10, 90);
   *
   *   // Interior vertices, counter-clockwise winding.
   *   beginContour();
   *   vertex(30, 30);
   *   vertex(30, 70);
   *   vertex(70, 70);
   *   vertex(70, 30);
   *   endContour(CLOSE);
   *
   *   // Stop drawing the shape.
   *   endShape(CLOSE);
   *
   *   describe('A white square with a square hole in its center drawn on a gray background.');
   * }
   *
   * @example
   * // Click and drag the mouse to view the scene from different angles.
   *
   * function setup() {
   *   createCanvas(100, 100, WEBGL);
   *
   *   describe('A white square with a square hole in its center drawn on a gray background.');
   * }
   *
   * function draw() {
   *   background(200);
   *
   *   // Enable orbiting with the mouse.
   *   orbitControl();
   *
   *   // Start drawing the shape.
   *   beginShape();
   *
   *   // Exterior vertices, clockwise winding.
   *   vertex(-40, -40);
   *   vertex(40, -40);
   *   vertex(40, 40);
   *   vertex(-40, 40);
   *
   *   // Interior vertices, counter-clockwise winding.
   *   beginContour();
   *   vertex(-20, -20);
   *   vertex(-20, 20);
   *   vertex(20, 20);
   *   vertex(20, -20);
   *   endContour(CLOSE);
   *
   *   // Stop drawing the shape.
   *   endShape(CLOSE);
   * }
   */
  fn.endContour = function(mode = constants.OPEN) {
    this._renderer.endContour(mode);
  };
}

export default customShapes;
export {
  Shape,
  Contour,
  ShapePrimitive,
  Vertex,
  Anchor,
  Segment,
  LineSegment,
  BezierSegment,
  SplineSegment,
  Point,
  Line,
  Triangle,
  Quad,
  ArcPrimitive,
  EllipsePrimitive,
  RectPrimitive,
  TriangleFan,
  TriangleStrip,
  QuadStrip,
  PrimitiveVisitor,
  PrimitiveToPath2DConverter,
  PrimitiveToVerticesConverter,
  PointAtLengthGetter
};

if (typeof p5 !== 'undefined') {
  customShapes(p5, p5.prototype);
}
