p5.embroider.js

(function (factory) {
  typeof define === 'function' && define.amd ? define(factory) :
  factory();
})((function () { 'use strict';

  /**
   * Class for writing Tajima DST embroidery files.
   * @class DSTWriter
   */


  class DSTWriter {
    constructor() {
      this.data = [];
      this.currentX = 0;
      this.currentY = 0;
      this.minX = Infinity;
      this.maxX = -Infinity;
      this.minY = Infinity;
      this.maxY = -Infinity;
      this.stitchCount = 0;
    }

    static JUMP = 1;
    static STITCH = 0;
    static COLOR_CHANGE = 2;
    static END = 3;

    bit(b) {
      return 1 << b;
    }

    encodeRecord(x, y, flag) {
      {
        console.log("Encoding record:", {
          x,
          y,
          flag:
            flag === DSTWriter.JUMP
              ? "JUMP"
              : flag === DSTWriter.STITCH
                ? "STITCH"
                : flag === DSTWriter.COLOR_CHANGE
                  ? "COLOR_CHANGE"
                  : flag,
        });
      }

      y = -y; // DST uses a different coordinate system
      let b0 = 0,
        b1 = 0,
        b2 = 0;

      switch (flag) {
        case DSTWriter.JUMP:
          b2 += this.bit(7);
        // fallthrough
        case DSTWriter.STITCH:
          b2 += this.bit(0);
          b2 += this.bit(1);
          if (x > 40) {
            b2 += this.bit(2);
            x -= 81;
          }
          if (x < -40) {
            b2 += this.bit(3);
            x += 81;
          }
          if (x > 13) {
            b1 += this.bit(2);
            x -= 27;
          }
          if (x < -13) {
            b1 += this.bit(3);
            x += 27;
          }
          if (x > 4) {
            b0 += this.bit(2);
            x -= 9;
          }
          if (x < -4) {
            b0 += this.bit(3);
            x += 9;
          }
          if (x > 1) {
            b1 += this.bit(0);
            x -= 3;
          }
          if (x < -1) {
            b1 += this.bit(1);
            x += 3;
          }
          if (x > 0) {
            b0 += this.bit(0);
            x -= 1;
          }
          if (x < 0) {
            b0 += this.bit(1);
            x += 1;
          }
          if (y > 40) {
            b2 += this.bit(5);
            y -= 81;
          }
          if (y < -40) {
            b2 += this.bit(4);
            y += 81;
          }
          if (y > 13) {
            b1 += this.bit(5);
            y -= 27;
          }
          if (y < -13) {
            b1 += this.bit(4);
            y += 27;
          }
          if (y > 4) {
            b0 += this.bit(5);
            y -= 9;
          }
          if (y < -4) {
            b0 += this.bit(4);
            y += 9;
          }
          if (y > 1) {
            b1 += this.bit(7);
            y -= 3;
          }
          if (y < -1) {
            b1 += this.bit(6);
            y += 3;
          }
          if (y > 0) {
            b0 += this.bit(7);
            y -= 1;
          }
          if (y < 0) {
            b0 += this.bit(6);
            y += 1;
          }
          break;
        case DSTWriter.COLOR_CHANGE:
          b2 = 0b11000011;
          break;
        case DSTWriter.END:
          b2 = 0b11110011;
          break;
      }
      return [b0, b1, b2];
    }

    move(x, y, flag = DSTWriter.STITCH) {
      if (x !== null && y !== null) {
        {
          console.log("Move called with:", {
            targetX: x,
            targetY: y,
            flag: flag === DSTWriter.JUMP ? "JUMP" : flag === DSTWriter.STITCH ? "STITCH" : flag,
            currentPosition: { x: this.currentX, y: this.currentY },
          });
        }

        let dx = Math.round(x) - this.currentX;
        let dy = Math.round(y) - this.currentY;

        while (Math.abs(dx) > 121 || Math.abs(dy) > 121) {
          let stepX = dx > 0 ? Math.min(dx, 121) : Math.max(dx, -121);
          let stepY = dy > 0 ? Math.min(dy, 121) : Math.max(dy, -121);

          let command = this.encodeRecord(stepX, stepY, DSTWriter.JUMP);
          this.data.push(...command);
          this.currentX += stepX;
          this.currentY += stepY;
          this.stitchCount++;

          dx -= stepX;
          dy -= stepY;
        }

        if (dx !== 0 || dy !== 0) {
          let command = this.encodeRecord(dx, dy, flag);
          this.data.push(...command);
          this.currentX += dx;
          this.currentY += dy;
          this.stitchCount++;
        }

        this.minX = Math.min(this.minX, this.currentX);
        this.maxX = Math.max(this.maxX, this.currentX);
        this.minY = Math.min(this.minY, this.currentY);
        this.maxY = Math.max(this.maxY, this.currentY);

        {
          console.log("After move:", {
            newPosition: { x: this.currentX, y: this.currentY },
            dx: dx,
            dy: dy,
          });
        }
      }
    }

    calculateBorderSize(points) {
      let minX = Infinity,
        maxX = -Infinity,
        minY = Infinity,
        maxY = -Infinity;

      // Log all trim points for debugging
      {
        const trimPoints = points.filter((p) => p.trim);
        if (trimPoints.length > 0) {
          console.log("Trim points in calculateBorderSize:", trimPoints);
        }
      }

      for (let point of points) {
        minX = Math.min(minX, point.x);
        maxX = Math.max(maxX, point.x);
        minY = Math.min(minY, point.y);
        maxY = Math.max(maxY, point.y);
      }

      const result = {
        left: Math.abs(Math.floor(minX)),
        top: Math.abs(Math.floor(minY)),
        right: Math.abs(Math.ceil(maxX)),
        bottom: Math.abs(Math.ceil(maxY)),
        width: Math.ceil(maxX - minX),
        height: Math.ceil(maxY - minY),
        // Add raw bounds for debugging
        bounds: {
          minX,
          maxX,
          minY,
          maxY,
        },
      };

      {
        console.log("Border calculation:", {
          points: points.length,
          result,
        });
      }

      return result;
    }

    generateDST(points, title) {
      {
        console.log("=== DSTWriter generateDST ===");
        console.log("Initial state:", {
          currentX: this.currentX,
          currentY: this.currentY,
        });
      }

      // Reset data and counters
      this.data = [];
      this.currentX = 0;
      this.currentY = 0;
      this.stitchCount = 0;
      this.colorChangeCount = 0;
      this.minX = Infinity;
      this.maxX = -Infinity;
      this.minY = Infinity;
      this.maxY = -Infinity;

      // Calculate border size before transformation
      let border = this.calculateBorderSize(points);
      {
        console.log("Original border size:", border);
      }

      // Transform points to center-origin coordinates
      const centerX = border.width / 2;
      const centerY = border.height / 2;

      {
        console.log("Transformation values:", {
          centerX,
          centerY,
          left: border.left,
          top: border.top,
          offset: {
            x: border.left + centerX,
            y: border.top + centerY,
          },
        });
      }

      const transformedPoints = points.map((point) => {
        // Create a new point object with all properties from the original point
        const newPoint = { ...point };

        // Transform coordinates to center-origin
        newPoint.x = point.x - (border.left + centerX);
        newPoint.y = point.y - (border.top + centerY);

        // Log transformation for trim points
        if (point.trim) {
          console.log("Transforming trim point:", {
            original: { x: point.x, y: point.y },
            transformed: { x: newPoint.x, y: newPoint.y },
            offset: { x: border.left + centerX, y: border.top + centerY },
          });
        }

        return newPoint;
      });

      {
        console.log("Coordinate transformation:", {
          centerX,
          centerY,
          originalFirstPoint: points[0],
          transformedFirstPoint: transformedPoints[0],
        });
      }

      // Recalculate border size after transformation
      border = this.calculateBorderSize(transformedPoints);
      {
        console.log("Transformed border size:", border);
      }

      // Generate stitches using transformed points
      for (let i = 0; i < transformedPoints.length; i++) {
        const point = transformedPoints[i];
        {
          console.log("Processing point:", i, point);
        }

        // Handle color change
        if (point.colorChange) {
          {
            console.log("Color change at point:", i);
          }
          // Add a color change command at the current position
          // In DST, we don't move but just insert a color change command
          this.data.push(...this.encodeRecord(0, 0, DSTWriter.COLOR_CHANGE));
          this.colorChangeCount++;
          continue;
        }

        // Handle thread trim
        if (point.trim) {
          {
            console.log("Thread trim at point:", i, {
              originalPoint: point,
              currentPosition: { x: this.currentX, y: this.currentY },
            });
          }

          // In DST format, thread trimming is signaled by a specific pattern of jump stitches
          // First, ensure we're at the correct position
          this.move(point.x, point.y, DSTWriter.JUMP);

          {
            console.log("After move to trim position:", {
              targetPosition: { x: point.x, y: point.y },
              actualPosition: { x: this.currentX, y: this.currentY },
            });
          }

          // Generate a zigzag pattern of 3 jumps that embroidery machines recognize as a trim command
          // These are small relative movements from the current position

          // First jump: up and right
          this.data.push(...this.encodeRecord(3, 3, DSTWriter.JUMP));
          this.currentX += 3;
          this.currentY += 3;
          this.stitchCount++;

          // Second jump: down and right
          this.data.push(...this.encodeRecord(3, -6, DSTWriter.JUMP));
          this.currentX += 3;
          this.currentY -= 6;
          this.stitchCount++;

          // Third jump: back to original position
          this.data.push(...this.encodeRecord(-6, 3, DSTWriter.JUMP));
          this.currentX -= 6;
          this.currentY += 3;
          this.stitchCount++;

          continue;
        }

        // Handle jump or stitch
        const flag = i === 0 || point.jump ? DSTWriter.JUMP : DSTWriter.STITCH;
        this.move(point.x, point.y, flag);

        {
          console.log("After move:", {
            point: i,
            position: { x: this.currentX, y: this.currentY },
            flag: flag === DSTWriter.JUMP ? "JUMP" : "STITCH",
          });
        }
      }

      // Add end record
      this.move(0, 0, DSTWriter.END);

      {
        console.log("Final state:", {
          stitchCount: this.stitchCount,
          colorChangeCount: this.colorChangeCount,
          bounds: {
            minX: this.minX,
            maxX: this.maxX,
            minY: this.minY,
            maxY: this.maxY,
          },
        });
      }

      // Create header
      let header = new Array(512).fill(0x20); // Fill with spaces
      let headerString =
        `LA:${title.padEnd(16)}\r` +
        `ST:${this.stitchCount.toString().padStart(7)}\r` +
        `CO:${this.colorChangeCount.toString().padStart(3)}\r` +
        `+X:${border.right.toString().padStart(5)}\r` +
        `-X:${Math.abs(border.left).toString().padStart(5)}\r` +
        `+Y:${border.bottom.toString().padStart(5)}\r` +
        `-Y:${Math.abs(border.top).toString().padStart(5)}\r` +
        `AX:+${Math.abs(this.currentX).toString().padStart(5)}\r` +
        `AY:+${Math.abs(this.currentY).toString().padStart(5)}\r` +
        `MX:+${(0).toString().padStart(5)}\r` +
        `MY:+${(0).toString().padStart(5)}\r` +
        `PD:******\r`;

      // Convert header string to byte array
      for (let i = 0; i < headerString.length; i++) {
        header[i] = headerString.charCodeAt(i);
      }
      header[headerString.length] = 0x1a; // EOF character

      // Combine header and data
      return new Uint8Array([...header, ...this.data]);
    }

    saveBytes(data, filename) {
      let blob = new Blob([data], { type: "application/octet-stream" });
      let link = document.createElement("a");
      link.href = URL.createObjectURL(blob);
      link.download = filename;

      // Prevent page refresh by handling the click event
      link.onclick = function (e) {
        // Let download happen, just prevent page refresh
        setTimeout(() => e.preventDefault(), 10);
        //restore download function

        // Clean up after download starts
        setTimeout(() => {
          URL.revokeObjectURL(link.href);
          document.body.removeChild(link);
        }, 100);
      };

      document.body.appendChild(link);
      link.click();
    }

    /**
     * Saves embroidery data as a DST file.
     * @memberof DSTWriter
     * @param {Array} points - Array of stitch points
     * @param {String} title - Title for the DST file header
     * @param {String} filename - Output filename
     */
    saveDST(points, title, filename) {
      let dstData = this.generateDST(points, title);
      this.saveBytes(dstData, filename);
      {
        console.log("DST file saved!");
      }
    }
  }

  // Add this check to support both direct browser usage and ES modules
  if (typeof exports !== "undefined") {
    exports.DSTWriter = DSTWriter;
  } else if (typeof window !== "undefined") {
    window.DSTWriter = DSTWriter;
  }

  // p5.js G-code Writer
  class GCodeWriter {
    constructor() {
      this.data = [];
      this.currentX = 0;
      this.currentY = 0;
      this.currentZ = 0;
      this.minX = Infinity;
      this.maxX = -Infinity;
      this.minY = Infinity;
      this.maxY = -Infinity;
    }

    addComment(comment) {
      this.data.push("(" + comment + ")");
    }

    move(x, y, z = null) {
      let command = "G0";
      if (x !== null) {
        command += ` X${x.toFixed(3)}`;
        this.currentX = x;
        this.minX = Math.min(this.minX, x);
        this.maxX = Math.max(this.maxX, x);
      }
      if (y !== null) {
        command += ` Y${y.toFixed(3)}`;
        this.currentY = y;
        this.minY = Math.min(this.minY, y);
        this.maxY = Math.max(this.maxY, y);
      }
      if (z !== null) {
        command += ` Z${z.toFixed(1)}`;
        this.currentZ = z;
      }
      this.data.push(command);
    }

    generateGCode(points, title) {
      this.addComment(`TITLE:${title}`);
      this.addComment(`STITCH_COUNT:${points.length}`);

      // Generate points
      this.move(0.0, 0.0);

      for (let i = 0; i < points.length; i++) {
        let point = points[i];
        this.move(point.x, point.y);
        this.move(null, null, 0.0);
        this.move(point.x, point.y);
        this.move(null, null, 1.0);
      }

      // Add final moves
      this.move(0.0, 0.0);
      this.data.push("M30");

      // Add extents information at the beginning
      this.data.unshift(
        `(EXTENTS_BOTTOM:${this.minY.toFixed(3)})`,
        `(EXTENTS_RIGHT:${this.maxX.toFixed(3)})`,
        `(EXTENTS_TOP:${this.maxY.toFixed(3)})`,
        `(EXTENTS_LEFT:${this.minX.toFixed(3)})`,
        `(EXTENTS_HEIGHT:${(this.maxY - this.minY).toFixed(3)})`,
        `(EXTENTS_WIDTH:${(this.maxX - this.minX).toFixed(3)})`,
        "G90 (use absolute coordinates)",
        "G21 (coordinates will be specified in millimeters)",
      );

      return this.data.join("\n");
    }

    saveGcode(points, title, filename) {
      const gcode = this.generateGCode(points, title);
      const blob = new Blob([gcode], { type: "text/plain" });
      const link = document.createElement("a");
      link.href = URL.createObjectURL(blob);
      link.download = filename;
      link.click();
      setTimeout(() => {
        URL.revokeObjectURL(link.href);
        document.body.removeChild(link);
      }, 100);
    }
  }

  let _DEBUG = false;

  // Allow external control of debug mode
  if (typeof window !== "undefined" && window._DEBUG !== undefined) {
    _DEBUG = window._DEBUG;
  }

  // Expose debug control
  function setDebugMode(enabled) {
    _DEBUG = enabled;
    if (typeof window !== "undefined") {
      window._DEBUG = enabled;
    }
  }

  (function (global) {
    const p5embroidery = global.p5embroidery || {};

    // Internal properties
    let _p5Instance;
    let _recording = false;
    let _drawMode = "stitch"; // 'stitch', 'p5', 'realistic'
    let _stitchData = {
      width: 0,
      height: 0,
      threads: [],
      pixelsPerUnit: 1,
      stitchCount: 0,
    };

    // Vertex properties
    let _shapeKind = null;
    let _vertices = [];
    let _contourVertices = [];
    let _contours = [];
    let _currentContour = [];
    let _isContour = false;

    let _strokeThreadIndex = 0;
    let _fillThreadIndex = 0;
    let _currentTransform = {
      matrix: [1, 0, 0, 0, 1, 0, 0, 0, 1]};

    // Embroidery settings
    const _embroiderySettings = {
      stitchLength: 3, // mm
      stitchWidth: 0,
      minStitchLength: 1, // mm
      resampleNoise: 0, // 0-1 range
      minimumPathLength: 0,
      maximumJoinDistance: 0,
      maximumStitchesPerSquareMm: 0,
      jumpThreshold: 10, // mm
      units: "mm",
    };

    // stroke mode constants
    const STROKE_MODE = {
      STRAIGHT: "straight",
      ZIGZAG: "zigzag",
      LINES: "lines",
      SASHIKO: "sashiko",
    };

    // Add fill mode constants
    const FILL_MODE = {
      TATAMI: "tatami",
      SATIN: "satin",
      SPIRAL: "spiral",
    };

    // Add stroke join constants
    const STROKE_JOIN = {
      ROUND: "round",
      MITER: "miter",
      BEVEL: "bevel",
    };

    let _doStroke = false; // Track if stroke is enabled
    let _currentStrokeMode = STROKE_MODE.STRAIGHT;
    let _currentStrokeJoin = STROKE_JOIN.ROUND; // Default to round joins

    let _doFill = false; // Track if fill is enabled
    let _currentFillMode = FILL_MODE.TATAMI;
    let _fillSettings = {
      stitchLength: 3, // mm
      stitchWidth: 0.2,
      minStitchLength: 0.5, // mm
      resampleNoise: 0, // 0-1 range
      angle: 0, // Angle in radians
      rowSpacing: 0.8, // Space between rows in mm
      tieDistance: 15, // Distance between tie-down stitches in mm
      alternateAngle: false, // Whether to alternate angles between shapes
      color: { r: 0, g: 0, b: 0 },
    };

    // Add a stroke settings object to match the other settings objects
    let _strokeSettings = {
      stitchLength: 3, // mm
      stitchWidth: 0.2,
      minStitchLength: 1, // mm
      resampleNoise: 0, // 0-1 range
      strokeWeight: 0, // Width of the embroidery line
      strokeMode: STROKE_MODE.STRAIGHT,
      strokeJoin: STROKE_JOIN.ROUND, // Add join setting
    };

    /**
     * Apply transformation matrix to a point
     * @private
     */
    function transformPoint(point, matrix) {
      const x = point.x * matrix[0] + point.y * matrix[3] + matrix[6];
      const y = point.x * matrix[1] + point.y * matrix[4] + matrix[7];
      return { x, y };
    }

    /**
     * Sets the stroke mode for embroidery stitches.
     * @method setStrokeMode
     * @for p5
     * @param {string} mode - The stroke mode to use ('zigzag', 'lines', or 'sashiko')
     * @example
     * function setup() {
     *   createCanvas(400, 400);
     *   beginRecord(this);
     *   setStrokeMode('zigzag');
     *   line(10, 10, 50, 50); // Will use zigzag stitch pattern
     * }
     */

    p5embroidery.setStrokeMode = function (mode) {
      if (Object.values(STROKE_MODE).includes(mode)) {
        _currentStrokeMode = mode;
        _strokeSettings.strokeMode = mode;
      } else {
        console.warn(`Invalid stroke mode: ${mode}. Using default: ${_currentStrokeMode}`);
      }
    };

    /**
     * Sets the stroke join mode for embroidery stitches.
     * @method setStrokeJoin
     * @for p5
     * @param {string} join - The stroke join mode to use ('round', 'miter', or 'bevel')
     * @example
     * function setup() {
     *   createCanvas(400, 400);
     *   beginRecord(this);
     *   setStrokeJoin('miter');
     *   beginShape();
     *   vertex(20, 20);
     *   vertex(50, 20);
     *   vertex(50, 50);
     *   endShape();
     * }
     */
    p5embroidery.setStrokeJoin = function (join) {
      if (Object.values(STROKE_JOIN).includes(join)) {
        _currentStrokeJoin = join;
        _strokeSettings.strokeJoin = join;
      } else {
        console.warn(`Invalid stroke join: ${join}. Using default: ${_currentStrokeJoin}`);
      }
    };

    /**
     * Sets the fill mode for embroidery fills.
     * @method setFillMode
     * @for p5
     * @param {string} mode - The fill mode to use ('tatami', 'satin', or 'spiral')
     */
    p5embroidery.setFillMode = function (mode) {
      if (Object.values(FILL_MODE).includes(mode)) {
        _currentFillMode = mode;
      } else {
        console.warn(`Invalid fill mode: ${mode}. Using default: ${_currentFillMode}`);
      }
    };

    /**
     * Sets the fill settings for embroidery.
     * @method setFillSettings
     * @for p5
     * @param {Object} settings - Fill settings object
     * @param {number} [settings.stitchLength] - Length of each stitch in mm
     * @param {number} [settings.stitchWidth] - Width of each stitch in mm
     * @param {number} [settings.minStitchLength] - Minimum stitch length in mm
     * @param {number} [settings.resampleNoise] - Amount of random variation (0-1)
     * @param {number} [settings.angle] - Fill angle in degrees
     * @param {number} [settings.rowSpacing] - Space between rows in mm
     * @param {number} [settings.tieDistance] - Distance between tie-down stitches in mm
     * @param {boolean} [settings.alternateAngle] - Whether to alternate angles between shapes
     */
    p5embroidery.setFillSettings = function (settings) {
      if (settings.stitchLength !== undefined) {
        _fillSettings.stitchLength = settings.stitchLength;
      }
      if (settings.stitchWidth !== undefined) {
        _fillSettings.stitchWidth = settings.stitchWidth;
      }
      if (settings.minStitchLength !== undefined) {
        _fillSettings.minStitchLength = settings.minStitchLength;
      }
      if (settings.resampleNoise !== undefined) {
        _fillSettings.resampleNoise = settings.resampleNoise;
      }

      if (settings.angle !== undefined) {
        _fillSettings.angle = (settings.angle * Math.PI) / 180; // Convert to radians
      }
      if (settings.rowSpacing !== undefined) {
        _fillSettings.rowSpacing = settings.rowSpacing;
      }
      if (settings.tieDistance !== undefined) {
        _fillSettings.tieDistance = settings.tieDistance;
      }
      if (settings.alternateAngle !== undefined) {
        _fillSettings.alternateAngle = settings.alternateAngle;
      }
    };

    /**
     * Thread class for storing color and stitch data.
     * @class Thread
     * @private
     */
    class Thread {
      /**
       * Creates a new Thread instance.
       * @constructor
       * @param {number} r - Red component (0-255)
       * @param {number} g - Green component (0-255)
       * @param {number} b - Blue component (0-255)
       * @param {number} [weight=0.2] - Weight of the thread in mm
       */
      constructor(r, g, b, weight = 0.2) {
        this.color = { r, g, b };
        this.runs = [];
        this.weight = weight;
      }
    }

    /**
     * Begins recording embroidery data.
     * @method beginRecord
     * @for p5
     * @param {p5} p5Instance - The p5.js sketch instance
     * @example
     * function setup() {
     *   createCanvas(400, 400);
     *   beginRecord(this);
     *   // Draw embroidery patterns here
     *   endRecord();
     * }
     */
    p5embroidery.beginRecord = function (p5Instance) {
      if (!p5Instance) {
        throw new Error("Invalid p5 instance provided to beginRecord().");
      }
      _p5Instance = p5Instance;
      _stitchData.width = p5Instance.width;
      _stitchData.height = p5Instance.height;
      _stitchData.threads = [new Thread(0, 0, 0, 0.2)]; // Start with a default black thread
      _recording = true;
      overrideP5Functions();
    };

    /**
     * Ends recording and prepares for export.
     * @method endRecord
     * @for p5
     * @example
     *
     *
     * function setup() {
     *   createCanvas(400, 400);
     *   beginRecord(this);
     *   // Draw embroidery patterns
     *   endRecord();
     * }
     *
     *
     */
    p5embroidery.endRecord = function () {
      _recording = false;
      restoreP5Functions();
      //exportEmbroidery(format);
    };

    let _originalBeginShapeFunc;
    function overrideBeginShapeFunction() {
      _originalBeginShapeFunc = window.beginShape;

      window.beginShape = function (kind) {
        if (_recording) {
          if (
            kind === window.POINTS ||
            kind === window.LINES ||
            kind === window.TRIANGLES ||
            kind === window.TRIANGLE_FAN ||
            kind === window.TRIANGLE_STRIP ||
            kind === window.QUADS ||
            kind === window.QUAD_STRIP
          ) {
            _shapeKind = kind;
          } else {
            _shapeKind = null;
          }

          _vertices = [];
          _contourVertices = [];
          _contours = [];
          _currentContour = [];

          if (_drawMode === "p5") {
            _originalBeginShapeFunc.apply(this, arguments);
          }
        } else {
          _originalBeginShapeFunc.apply(this, arguments);
        }
      };
    }

    let _originalEndShapeFunc;
    function overrideEndShapeFunction() {
      _originalEndShapeFunc = window.endShape;

      window.endShape = function (mode, count = 1) {
        if (count < 1) {
          console.log("🪡 p5.embroider says: You can not have less than one instance");
          count = 1;
        }
        if (_recording) {
          console.log("endShape", _vertices, _vertices.length);
          if (_vertices.length === 0) {
            console.log("🪡 p5.embroider says: No vertices to draw");
            return this;
          }
          if (_DEBUG) {
            console.log("endShape", _vertices, _vertices.length);
            console.log("_doStroke", _doStroke);
            console.log("_doFill", _doFill);
          }
          if (!_doStroke && !_doFill) {
            console.log("🪡 p5.embroider says: _doStroke and _doFill are both false");
            return this;
          }

          const closeShape = mode === window.CLOSE;

          if (closeShape && !_isContour) {
            _vertices.push(_vertices[0]);
          }
          if (_doFill) {
            // Convert vertices to pathPoints format for the fill function
            const mainPath = _vertices.map((v) => ({
              x: v.x,
              y: v.y,
            }));

            let fillStitches = [];

            if (_contours.length > 0) {
              // Fill with contours
              if (_DEBUG) console.log("Filling shape with", _contours.length, "contours");
              fillStitches = createTatamiFillWithContours(mainPath, _contours, _fillSettings);
            } else {
              // Simple fill without contours
              fillStitches = createTatamiFillFromPath(mainPath, _fillSettings);
            }

            if (fillStitches && fillStitches.length > 0) {
              _stitchData.threads[_fillThreadIndex].runs.push(fillStitches);

              // Draw fill stitches in visual modes
              if (_drawMode === "stitch" || _drawMode === "realistic") {
                drawStitches(fillStitches, _fillThreadIndex);
              }
            }
          }

          //convert vertices to embroidery stitches
          const stitches = p5embroidery.convertVerticesToStitches(_vertices, _strokeSettings);

          // Debug log
          if (_DEBUG) {
            console.log("endShape: Converted vertices to stitches:", {
              vertices: _vertices.length,
              stitches: stitches.length,
              shapeKind: _shapeKind,
              mode: _drawMode,
            });
          }

          //add stitches to the embroidery data
          _stitchData.threads[_strokeThreadIndex].runs.push(stitches);

          if (_drawMode === "stitch" || _drawMode === "realistic") {
            console.log("Drawing stitches:", {
              count: stitches.length,
              threadIndex: _strokeThreadIndex,
              mode: _drawMode,
              firstStitch: stitches.length > 0 ? stitches[0] : null,
              lastStitch: stitches.length > 0 ? stitches[stitches.length - 1] : null,
            });
            drawStitches(stitches, _strokeThreadIndex);
          } else if (_drawMode === "p5") {
            _originalEndShapeFunc.call(_p5Instance, mode, count);
          }
          _isContour = false;

          // Reset contour vertices for next shape
          _contourVertices = [];
          _contours = [];
          _currentContour = [];

          // If the shape is closed, the first element was added as last element.
          // We must remove it again to prevent the list of vertices from growing
          // over successive calls to endShape(CLOSE)
          if (closeShape) {
            _vertices.pop();
          }

          // After drawing both shapes
          console.log(
            "Thread runs:",
            _stitchData.threads[_strokeThreadIndex].runs.map((run) => ({
              length: run.length,
              first: run.length > 0 ? { x: run[0].x, y: run[0].y } : null,
              last: run.length > 0 ? { x: run[run.length - 1].x, y: run[run.length - 1].y } : null,
            })),
          );
        } else {
          _originalEndShapeFunc.apply(this, arguments);
        }

        return this;
      };
    }

    let _originalVertexFunc;
    function overrideVertexFunction() {
      _originalVertexFunc = window.vertex;

      window.vertex = function (x, y, moveTo, u, v) {
        if (_recording) {
          // Apply current transformation to the vertex coordinates
          const transformedPoint = transformPoint({ x, y }, _currentTransform.matrix);
          
          // Create a vertex object with named properties instead of an array
          const vert = {
            x: transformedPoint.x,
            y: transformedPoint.y,
            u: u || 0,
            v: v || 0,
            isVert: true,
          };

          if (moveTo) {
            vert.moveTo = moveTo;
          }

          if (_drawMode === "p5") {
            _originalVertexFunc.call(_p5Instance, mmToPixel(transformedPoint.x), mmToPixel(transformedPoint.y), moveTo, u, v);
          }

          // Add to appropriate container based on contour state
          if (_isContour) {
            _currentContour.push({ x: transformedPoint.x, y: transformedPoint.y });
            if (_DEBUG) console.log("Added to contour (transformed):", { x: transformedPoint.x, y: transformedPoint.y });
          } else {
            _vertices.push(vert);
            if (_DEBUG) console.log("Added to vertices (transformed):", vert);
          }
        } else {
          let args = [mmToPixel(x), mmToPixel(y), moveTo, u, v];
          _originalVertexFunc.apply(this, args);
        }
      };
    }

    /**
     * Overrides p5.js bezierVertex() function.
     * @private
     */
    let _originalBezierVertexFunc;
    function overrideBezierVertexFunction() {
      _originalBezierVertexFunc = window.bezierVertex;

      window.bezierVertex = function (x2, y2, x3, y3, x4, y4) {
        if (_recording) {
          // Get the last vertex as the starting point - check both main vertices and current contour
          let lastVertex;
          if (_isContour) {
            if (_currentContour.length === 0) {
              console.warn("bezierVertex() called without a previous vertex in contour");
              return;
            }
            lastVertex = _currentContour[_currentContour.length - 1];
          } else {
            if (_vertices.length === 0) {
              console.warn("bezierVertex() called without a previous vertex");
              return;
            }
            lastVertex = _vertices[_vertices.length - 1];
          }
          const x1 = lastVertex.x;
          const y1 = lastVertex.y;

          // Generate bezier curve points
          const bezierPoints = generateBezierPoints(x1, y1, x2, y2, x3, y3, x4, y4);

          // Add all points except the first one (which is the last vertex)
          for (let i = 1; i < bezierPoints.length; i++) {
            const point = bezierPoints[i];

            if (_isContour) {
              _currentContour.push({ x: point.x, y: point.y });
            } else {
              const vert = {
                x: point.x,
                y: point.y,
                u: 0,
                v: 0,
                isVert: true,
                isBezier: true,
              };
              _vertices.push(vert);
            }

            if (_drawMode === "p5") {
              if (i === 1) {
                // Call bezierVertex with p5 for the first segment
                _originalBezierVertexFunc.call(
                  _p5Instance,
                  mmToPixel(x2),
                  mmToPixel(y2),
                  mmToPixel(x3),
                  mmToPixel(y3),
                  mmToPixel(x4),
                  mmToPixel(y4),
                );
              }
            }
          }
          if (_DEBUG) console.log("bezierVertex added points:", bezierPoints.length - 1);
        } else {
          _originalBezierVertexFunc.apply(this, arguments);
        }
      };
    }

    /**
     * Overrides p5.js quadraticVertex() function.
     * @private
     */
    let _originalQuadraticVertexFunc;
    function overrideQuadraticVertexFunction() {
      _originalQuadraticVertexFunc = window.quadraticVertex;

      window.quadraticVertex = function (cx, cy, x3, y3) {
        if (_recording) {
          // Get the last vertex as the starting point - check both main vertices and current contour
          let lastVertex;
          if (_isContour) {
            if (_currentContour.length === 0) {
              console.warn("quadraticVertex() called without a previous vertex in contour");
              return;
            }
            lastVertex = _currentContour[_currentContour.length - 1];
          } else {
            if (_vertices.length === 0) {
              console.warn("quadraticVertex() called without a previous vertex");
              return;
            }
            lastVertex = _vertices[_vertices.length - 1];
          }
          const x1 = lastVertex.x;
          const y1 = lastVertex.y;

          // Generate quadratic bezier curve points (convert to cubic bezier)
          const quadraticPoints = generateQuadraticPoints(x1, y1, cx, cy, x3, y3);

          // Add all points except the first one (which is the last vertex)
          for (let i = 1; i < quadraticPoints.length; i++) {
            const point = quadraticPoints[i];

            if (_isContour) {
              _currentContour.push({ x: point.x, y: point.y });
            } else {
              const vert = {
                x: point.x,
                y: point.y,
                u: 0,
                v: 0,
                isVert: true,
                isQuadratic: true,
              };
              _vertices.push(vert);
            }

            if (_drawMode === "p5") {
              if (i === 1) {
                // Call quadraticVertex with p5 for the first segment
                _originalQuadraticVertexFunc.call(
                  _p5Instance,
                  mmToPixel(cx),
                  mmToPixel(cy),
                  mmToPixel(x3),
                  mmToPixel(y3),
                );
              }
            }
          }
          if (_DEBUG) console.log("quadraticVertex added points:", quadraticPoints.length - 1);
        } else {
          _originalQuadraticVertexFunc.apply(this, arguments);
        }
      };
    }

    /**
     * Overrides p5.js curveVertex() function.
     * @private
     */
    let _originalCurveVertexFunc;
    function overrideCurveVertexFunction() {
      _originalCurveVertexFunc = window.curveVertex;

      window.curveVertex = function (x, y) {
        if (_recording) {
          // Add to contour vertices for curve calculation
          _contourVertices.push({ x, y });

          // For curve vertices, we need at least 4 points to generate a curve segment
          if (_contourVertices.length >= 4) {
            const len = _contourVertices.length;
            const p0 = _contourVertices[len - 4];
            const p1 = _contourVertices[len - 3];
            const p2 = _contourVertices[len - 2];
            const p3 = _contourVertices[len - 1];

            // Generate curve points for this segment
            const curvePoints = generateCurvePoints(p0.x, p0.y, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);

            // If this is the first curve segment, add all points
            // Otherwise, add all points except the first (to avoid duplication)
            let startIdx;
            if (_isContour) {
              startIdx = _currentContour.length === 0 ? 0 : 1;
            } else {
              startIdx = _vertices.length === 0 ? 0 : 1;
            }

            for (let i = startIdx; i < curvePoints.length; i++) {
              const point = curvePoints[i];

              if (_isContour) {
                _currentContour.push({ x: point.x, y: point.y });
              } else {
                const vert = {
                  x: point.x,
                  y: point.y,
                  u: 0,
                  v: 0,
                  isVert: true,
                  isCurve: true,
                };
                _vertices.push(vert);
              }
            }
          }

          if (_drawMode === "p5") {
            _originalCurveVertexFunc.call(_p5Instance, mmToPixel(x), mmToPixel(y));
          }
          if (_DEBUG) console.log("curveVertex added, contour length:", _contourVertices.length);
        } else {
          _originalCurveVertexFunc.apply(this, arguments);
        }
      };
    }

    /**
     * Generates points along a quadratic Bezier curve.
     * @private
     */
    function generateQuadraticPoints(x1, y1, cx, cy, x3, y3) {
      const points = [];
      const bezierDetail = _p5Instance._bezierDetail || 20;

      for (let i = 0; i <= bezierDetail; i++) {
        const t = i / bezierDetail;
        const x = quadraticBezierPoint(x1, cx, x3, t);
        const y = quadraticBezierPoint(y1, cy, y3, t);
        points.push({ x, y });
      }

      return points;
    }

    /**
     * Calculate a point on a quadratic Bezier curve.
     * @private
     */
    function quadraticBezierPoint(a, b, c, t) {
      const mt = 1 - t;
      const mt2 = mt * mt;
      const t2 = t * t;
      return a * mt2 + 2 * b * mt * t + c * t2;
    }

    /**
     * Converts vertices to embroidery stitches.
     * @method convertVerticesToStitches
     * @private
     * @param {Array} vertices - Array of vertex objects
     * @param {Object} strokeSettings - Settings for the stroke
     * @returns {Array} Array of stitch points
     */
    p5embroidery.convertVerticesToStitches = function (vertices, strokeSettings) {
      let stitches = [];

      if (!vertices || vertices.length < 2) {
        if (_DEBUG) console.log("convertVerticesToStitches: insufficient vertices", vertices);
        return stitches;
      }

      // Extract x,y coordinates from vertex objects for compatibility with path functions
      const pathPoints = vertices.map((v) => ({
        x: v.x,
        y: v.y,
      }));

      if (_DEBUG) {
        console.log("convertVerticesToStitches input:", {
          vertexCount: vertices.length,
          pathPoints: pathPoints,
          strokeWeight: strokeSettings.strokeWeight,
          strokeMode: strokeSettings.strokeMode,
          strokeJoin: strokeSettings.strokeJoin,
        });
      }

      // If we have a stroke weight and multiple vertices, use join-aware stitching
      if (strokeSettings.strokeWeight > 0 && vertices.length > 2) {
        if (_DEBUG) console.log("Using convertPathToStitchesWithJoins");
        return convertPathToStitchesWithJoins(pathPoints, strokeSettings);
      }
      // If we have a stroke weight, use the appropriate path-based function
      else if (strokeSettings.strokeWeight > 0) {
        if (_DEBUG) console.log("Using stroke mode:", strokeSettings.strokeMode);
        switch (strokeSettings.strokeMode) {
          case STROKE_MODE.STRAIGHT:
            return straightLineStitchFromPath(pathPoints, strokeSettings);
          case STROKE_MODE.ZIGZAG:
            return zigzagStitchFromPath(pathPoints, strokeSettings);
          case STROKE_MODE.LINES:
            return multiLineStitchFromPath(pathPoints, strokeSettings);
          case STROKE_MODE.SASHIKO:
            return sashikoStitchFromPath(pathPoints, strokeSettings);
          default:
            // For simple paths, use the convertPathToStitches function
            return convertPathToStitches(pathPoints, strokeSettings);
        }
      } else {
        if (_DEBUG) console.log("Using generic convertPathToStitches");
        // For normal width lines, just use the generic path to stitches conversion
        return convertPathToStitches(pathPoints, strokeSettings);
      }
    };

    /**
     * Overrides p5.js line() function to record embroidery stitches.
     * @private
     */
    let _originalLineFunc;
    function overrideLineFunction() {
      _originalLineFunc = window.line;
      window.line = function (x1, y1, x2, y2) {
        if (_recording) {
          if (_doStroke) {
            let stitches = convertLineToStitches(x1, y1, x2, y2, _strokeSettings);
            _stitchData.threads[_strokeThreadIndex].runs.push(stitches);

            if (_drawMode === "stitch" || _drawMode === "realistic") {
              drawStitches(stitches, _strokeThreadIndex);
            } else if (_drawMode === "p5") {
              _originalStrokeWeightFunc.call(_p5Instance, mmToPixel(_strokeSettings.strokeWeight));
              _originalLineFunc.call(_p5Instance, mmToPixel(x1), mmToPixel(y1), mmToPixel(x2), mmToPixel(y2));
            }
          }
        } else {
          _originalLineFunc.apply(this, arguments);
        }
      };
    }

    /**
     * Overrides p5.js curve() function to record embroidery stitches.
     * @private
     */
    let _originalCurveFunc;
    function overrideCurveFunction() {
      _originalCurveFunc = window.curve;
      window.curve = function (x1, y1, x2, y2, x3, y3, x4, y4) {
        if (_recording) {
          if (_doStroke) {
            // Generate curve points using Catmull-Rom spline
            const curvePoints = generateCurvePoints(x1, y1, x2, y2, x3, y3, x4, y4);
            let stitches = p5embroidery.convertVerticesToStitches(
              curvePoints.map((p) => ({ x: p.x, y: p.y, isVert: true })),
              _strokeSettings,
            );
            _stitchData.threads[_strokeThreadIndex].runs.push(stitches);

            if (_drawMode === "stitch" || _drawMode === "realistic") {
              drawStitches(stitches, _strokeThreadIndex);
            } else if (_drawMode === "p5") {
              _originalStrokeWeightFunc.call(_p5Instance, mmToPixel(_strokeSettings.strokeWeight));
              _originalCurveFunc.call(
                _p5Instance,
                mmToPixel(x1),
                mmToPixel(y1),
                mmToPixel(x2),
                mmToPixel(y2),
                mmToPixel(x3),
                mmToPixel(y3),
                mmToPixel(x4),
                mmToPixel(y4),
              );
            }
          }
        } else {
          _originalCurveFunc.apply(this, arguments);
        }
      };
    }

    /**
     * Overrides p5.js bezier() function to record embroidery stitches.
     * @private
     */
    let _originalBezierFunc;
    function overrideBezierFunction() {
      _originalBezierFunc = window.bezier;
      window.bezier = function (x1, y1, x2, y2, x3, y3, x4, y4) {
        if (_recording) {
          if (_doStroke) {
            // Generate bezier curve points
            const bezierPoints = generateBezierPoints(x1, y1, x2, y2, x3, y3, x4, y4);
            let stitches = p5embroidery.convertVerticesToStitches(
              bezierPoints.map((p) => ({ x: p.x, y: p.y, isVert: true })),
              _strokeSettings,
            );
            _stitchData.threads[_strokeThreadIndex].runs.push(stitches);

            if (_drawMode === "stitch" || _drawMode === "realistic") {
              drawStitches(stitches, _strokeThreadIndex);
            } else if (_drawMode === "p5") {
              _originalStrokeWeightFunc.call(_p5Instance, mmToPixel(_strokeSettings.strokeWeight));
              _originalBezierFunc.call(
                _p5Instance,
                mmToPixel(x1),
                mmToPixel(y1),
                mmToPixel(x2),
                mmToPixel(y2),
                mmToPixel(x3),
                mmToPixel(y3),
                mmToPixel(x4),
                mmToPixel(y4),
              );
            }
          }
        } else {
          _originalBezierFunc.apply(this, arguments);
        }
      };
    }

    /**
     * Generates points along a Catmull-Rom curve.
     * @private
     */
    function generateCurvePoints(x1, y1, x2, y2, x3, y3, x4, y4) {
      const points = [];
      const curveDetail = _p5Instance._curveDetail || 20;

      for (let i = 0; i <= curveDetail; i++) {
        const t = i / curveDetail;
        const x = curvePoint(x1, x2, x3, x4, t);
        const y = curvePoint(y1, y2, y3, y4, t);
        points.push({ x, y });
      }

      return points;
    }

    /**
     * Generates points along a cubic Bezier curve.
     * @private
     */
    function generateBezierPoints(x1, y1, x2, y2, x3, y3, x4, y4) {
      const points = [];
      const bezierDetail = _p5Instance._bezierDetail || 20;

      for (let i = 0; i <= bezierDetail; i++) {
        const t = i / bezierDetail;
        const x = bezierPoint(x1, x2, x3, x4, t);
        const y = bezierPoint(y1, y2, y3, y4, t);
        points.push({ x, y });
      }

      return points;
    }

    /**
     * Calculate a point on a Catmull-Rom curve.
     * @private
     */
    function curvePoint(a, b, c, d, t) {
      const t2 = t * t;
      const t3 = t2 * t;
      return 0.5 * (2 * b + (-a + c) * t + (2 * a - 5 * b + 4 * c - d) * t2 + (-a + 3 * b - 3 * c + d) * t3);
    }

    /**
     * Calculate a point on a cubic Bezier curve.
     * @private
     */
    function bezierPoint(a, b, c, d, t) {
      const t2 = t * t;
      const t3 = t2 * t;
      const mt = 1 - t;
      const mt2 = mt * mt;
      const mt3 = mt2 * mt;
      return a * mt3 + 3 * b * mt2 * t + 3 * c * mt * t2 + d * t3;
    }

    /**
     * Overrides p5.js stroke() function to select thread color.
     * @private
     */
    let _originalStrokeFunc;
    function overrideStrokeFunction() {
      _originalStrokeFunc = window.stroke;
      window.stroke = function () {
        if (_recording) {
          // Get color values from arguments
          let r, g, b;

          if (arguments.length === 1) {
            // Single value or string color
            if (typeof arguments[0] === "string") {
              // Parse color string (e.g., '#FF0000' or 'red')
              const colorObj = _p5Instance.color(arguments[0]);
              r = _p5Instance.red(colorObj);
              g = _p5Instance.green(colorObj);
              b = _p5Instance.blue(colorObj);
            } else {
              // Grayscale value
              r = g = b = arguments[0];
            }
          } else if (arguments.length === 3) {
            // RGB values
            r = arguments[0];
            g = arguments[1];
            b = arguments[2];
          } else {
            // Default to black if invalid arguments
            r = g = b = 0;
          }

          // Check if we already have a thread with this color
          let threadIndex = -1;
          for (let i = 0; i < _stitchData.threads.length; i++) {
            const threadColor = _stitchData.threads[i].color;
            if (threadColor.r === r && threadColor.g === g && threadColor.b === b) {
              threadIndex = i;
              break;
            }
          }

          if (threadIndex === -1) {
            // Create a new thread with this color
            _stitchData.threads.push(new Thread(r, g, b));
            threadIndex = _stitchData.threads.length - 1;
          }

          // If we're changing to a different thread and have existing stitches,
          // add a thread trim command at the current position
          if (_strokeThreadIndex !== threadIndex && _stitchData.threads[_strokeThreadIndex] !== undefined) {
            trimThread();
          }

          // Set the current thread index
          _strokeThreadIndex = threadIndex;
          _doStroke = true;

          _originalStrokeFunc.apply(this, arguments);
        } else {
          _originalStrokeFunc.apply(this, arguments);
        }
      };
    }

    /**
     * Overrides p5.js noStroke() function to disable embroidery strokes.
     * @private
     */
    let _originalNoStrokeFunc;
    function overrideNoStrokeFunction() {
      _originalNoStrokeFunc = window.noStroke;
      window.noStroke = function () {
        if (_recording) {
          _doStroke = false;
        }
        _originalNoStrokeFunc.apply(this, arguments);
      };
    }

    /**
     * Overrides p5.js fill() function to handle embroidery fills.
     * @private
     */
    let _originalFillFunc;
    function overrideFillFunction() {
      _originalFillFunc = window.fill;
      window.fill = function () {
        if (_recording) {
          // Get color values from arguments
          let r, g, b;

          if (arguments.length === 1) {
            // Single value or string color
            if (typeof arguments[0] === "string") {
              // Parse color string (e.g., '#FF0000' or 'red')
              const colorObj = _p5Instance.color(arguments[0]);
              r = _p5Instance.red(colorObj);
              g = _p5Instance.green(colorObj);
              b = _p5Instance.blue(colorObj);
            } else {
              // Grayscale value
              r = g = b = arguments[0];
            }
          } else if (arguments.length === 3) {
            // RGB values
            r = arguments[0];
            g = arguments[1];
            b = arguments[2];
          } else {
            // Default to black if invalid arguments
            r = g = b = 0;
          }

          // Check if we already have a thread with this color
          let threadIndex = -1;
          for (let i = 0; i < _stitchData.threads.length; i++) {
            const thread = _stitchData.threads[i];
            if (thread.color.r === r && thread.color.g === g && thread.color.b === b) {
              threadIndex = i;
              break;
            }
          }

          if (threadIndex === -1) {
            // Create a new thread with this color
            _stitchData.threads.push(new Thread(r, g, b));
            threadIndex = _stitchData.threads.length - 1;
          }

          // Set the current thread index
          _fillThreadIndex = threadIndex;

          // Store the fill state and color
          _doFill = true;
          _fillSettings.color = { r, g, b };

          _originalFillFunc.apply(this, arguments);
        } else {
          _originalFillFunc.apply(this, arguments);
        }
      };
    }

    /**
     * Overrides p5.js noFill() function to disable embroidery fills.
     * @private
     */
    let _originalNoFillFunc;
    function overrideNoFillFunction() {
      _originalNoFillFunc = window.noFill;
      window.noFill = function () {
        if (_recording) {
          _doFill = false;
          _fillSettings.color = null;
        }
        _originalNoFillFunc.apply(this, arguments);
      };
    }

    /**
     * Overrides p5.js strokeWeight() function to record embroidery stitches.
     * @private
     */
    let _originalStrokeWeightFunc;
    function overrideStrokeWeightFunction() {
      _originalStrokeWeightFunc = window.strokeWeight;

      window.strokeWeight = function (weight) {
        if (_recording) {
          // Set the stroke weight in the stroke settings
          _strokeSettings.strokeWeight = weight;
          //_embroiderySettings.stitchWidth = weight;

          _originalStrokeWeightFunc.call(this, weight);
        } else {
          _originalStrokeWeightFunc.apply(this, arguments);
        }
      };
    }

    /**
     * Overrides p5.js strokeJoin() function to set embroidery stroke join mode.
     * @private
     */
    let _originalStrokeJoinFunc;
    function overrideStrokeJoinFunction() {
      _originalStrokeJoinFunc = window.strokeJoin;

      window.strokeJoin = function (join) {
        if (_recording) {
          // Map p5.js constants to our internal format
          let mappedJoin;
          if (join === window.ROUND || join === "round") {
            mappedJoin = STROKE_JOIN.ROUND;
          } else if (join === window.MITER || join === "miter") {
            mappedJoin = STROKE_JOIN.MITER;
          } else if (join === window.BEVEL || join === "bevel") {
            mappedJoin = STROKE_JOIN.BEVEL;
          } else {
            console.warn(`Invalid stroke join: ${join}. Using default: ${_currentStrokeJoin}`);
            mappedJoin = _currentStrokeJoin;
          }

          _currentStrokeJoin = mappedJoin;
          _strokeSettings.strokeJoin = mappedJoin;

          _originalStrokeJoinFunc.call(this, join);
        } else {
          _originalStrokeJoinFunc.apply(this, arguments);
        }
      };
    }

    /**
     * Overrides p5.js ellipse() function to record embroidery stitches.
     * @private
     */
    let _originalEllipseFunc;
    function overrideEllipseFunction() {
      _originalEllipseFunc = window.ellipse;
      window.ellipse = function (x, y, w, h) {
        if (_recording) {
          // Calculate radius values
          let radiusX = w / 2;
          let radiusY = h / 2;

          // Generate path points for the ellipse
          let pathPoints = [];
          let numSteps = Math.max(Math.ceil((Math.PI * (radiusX + radiusY)) / _embroiderySettings.stitchLength), 12);

          // Generate points along the ellipse, starting at 0 degrees (right side of ellipse)
          for (let i = 0; i <= numSteps; i++) {
            let angle = (i / numSteps) * Math.PI * 2;
            let pointX = x + Math.cos(angle) * radiusX;
            let pointY = y + Math.sin(angle) * radiusY;

            // Store in mm (internal format)
            pathPoints.push({
              x: pointX,
              y: pointY,
            });
          }

          // Close the path by adding the first point again
          pathPoints.push({
            x: pathPoints[0].x,
            y: pathPoints[0].y,
          });

          // Record the stitches if we're recording
          if (_recording) {
            if (_doFill) {
              // Convert vertices to pathPoints format for the fill function
              const fillStitches = createTatamiFillFromPath(pathPoints, _fillSettings);
              _stitchData.threads[_fillThreadIndex].runs.push(fillStitches);

              // Draw fill stitches in visual modes
              if (_drawMode === "stitch" || _drawMode === "realistic") {
                drawStitches(fillStitches, _fillThreadIndex);
              }
            }

            // Get the current position (in mm)
            let currentX, currentY;
            if (
              _stitchData.threads[_strokeThreadIndex].runs.length === 0 ||
              _stitchData.threads[_strokeThreadIndex].runs[_stitchData.threads[_strokeThreadIndex].runs.length - 1]
                .length === 0
            ) {
              // If there are no runs or the last run is empty, use the first point on the ellipse
              // (at 0 degrees) as the starting point, not the center
              currentX = pathPoints[0].x;
              currentY = pathPoints[0].y;
            } else {
              // Otherwise, use the last stitch position (already in mm)
              let lastRun =
                _stitchData.threads[_strokeThreadIndex].runs[_stitchData.threads[_strokeThreadIndex].runs.length - 1];
              let lastStitch = lastRun[lastRun.length - 1];
              currentX = lastStitch.x;
              currentY = lastStitch.y;
            }

            // Add a jump stitch to the first point of the ellipse if needed
            if (
              Math.sqrt(Math.pow(pathPoints[0].x - currentX, 2) + Math.pow(pathPoints[0].y - currentY, 2)) >
              _embroiderySettings.jumpThreshold
            ) {
              _stitchData.threads[_strokeThreadIndex].runs.push([
                {
                  x: currentX,
                  y: currentY,
                  command: "jump",
                },
                {
                  x: pathPoints[0].x,
                  y: pathPoints[0].y,
                },
              ]);
            }

            // Convert path points to stitches based on current stroke mode
            let stitches;
            if (_strokeSettings.strokeWeight > 0) {
              switch (_strokeSettings.strokeMode) {
                case STROKE_MODE.ZIGZAG:
                  stitches = zigzagStitchFromPath(pathPoints, _strokeSettings);
                  break;
                case STROKE_MODE.LINES:
                  stitches = multiLineStitchFromPath(pathPoints, _strokeSettings);
                  break;
                case STROKE_MODE.SASHIKO:
                  stitches = sashikoStitchFromPath(pathPoints, _strokeSettings);
                  break;
                default:
                  stitches = straightLineStitchFromPath(pathPoints, _strokeSettings);
              }
            } else {
              // If no stroke weight specified, use straight line stitching
              stitches = straightLineStitchFromPath(pathPoints, _strokeSettings);
            }

            // Add the ellipse stitches
            _stitchData.threads[_strokeThreadIndex].runs.push(stitches);

            // Draw the stitches
            if (_drawMode === "p5") {
              _originalStrokeWeightFunc.call(this, mmToPixel(_strokeSettings.strokeWeight));
              _originalEllipseFunc.call(this, mmToPixel(x), mmToPixel(y), mmToPixel(w), mmToPixel(h));
            } else {
              drawStitches(stitches, _strokeThreadIndex);
            }
          }
        } else {
          _originalEllipseFunc.apply(this, arguments);
        }
      };
    }

    /**
     * Overrides p5.js circle() function to record embroidery stitches.
     * @private
     */
    let _originalCircleFunc;
    function overrideCircleFunction() {
      _originalCircleFunc = window.circle;
      window.circle = function (x, y, r) {
        if (_recording) {
          window.ellipse.call(this, x, y, r, r);
        } else {
          _originalCircleFunc.apply(this, arguments);
        }
      };
    }

    /**
     * Overrides p5.js point() function to record embroidery stitches.
     * @private
     */
    let _originalPointFunc;
    function overridePointFunction() {
      _originalPointFunc = window.point;
      window.point = function (x, y) {
        if (_recording) {
          // For point, we just add a single stitch
          let stitches = [
            {
              x: x,
              y: y,
            },
          ];
          _stitchData.threads[_strokeThreadIndex].runs.push(stitches);

          if (_drawMode === "stitch" || _drawMode === "realistic" || _drawMode === "p5") {
            _p5Instance.push();
            _originalStrokeFunc.call(_p5Instance, 255, 0, 0); // Red for stitch points
            _originalStrokeWeightFunc.call(_p5Instance, 3);
            _originalPointFunc.call(_p5Instance, mmToPixel(x), mmToPixel(y));
            _p5Instance.pop();
          }
        } else {
          _originalStrokeWeightFunc.call(this, mmToPixel(_strokeSettings.strokeWeight));
          _originalPointFunc.apply(this, arguments);
        }
      };
    }

    /**
     * Overrides p5.js rect() function to handle embroidery fills.
     * @private
     */
    let _originalRectFunc;
    function overrideRectFunction() {
      _originalRectFunc = window.rect;
      window.rect = function (x, y, w, h, ...cornerRs) {
        if (_recording) {
          const mode = _p5Instance._renderer._rectMode;
          let x1, y1;

          if (mode === _p5Instance.CENTER) {
            x1 = x - w / 2;
            y1 = y - h / 2;
          } else if (mode === _p5Instance.CORNERS) {
            // In CORNERS mode, w is x2 and h is y2. Re-calculate w and h to be width and height.
            w = w - x;
            h = h - y;
            x1 = x;
            y1 = y;
          } else {
            // CORNER mode (default)
            x1 = x;
            y1 = y;
          }

          const x2 = x1 + w;
          const y2 = y1 + h;

          let tl = 0,
            tr = 0,
            br = 0,
            bl = 0;
          if (cornerRs.length === 1 && cornerRs[0] !== undefined) {
            tl = tr = br = bl = cornerRs[0];
          } else if (cornerRs.length >= 4) {
            tl = cornerRs[0] || 0;
            tr = cornerRs[1] || 0;
            br = cornerRs[2] || 0;
            bl = cornerRs[3] || 0;
          }

          const halfW = Math.abs(w) / 2;
          const halfH = Math.abs(h) / 2;
          tl = Math.min(tl, halfW, halfH);
          tr = Math.min(tr, halfW, halfH);
          br = Math.min(br, halfW, halfH);
          bl = Math.min(bl, halfW, halfH);

          const pathPoints = [];

          // Check if we have any corner radii
          const hasCorners = tl > 0 || tr > 0 || br > 0 || bl > 0;

          if (!hasCorners) {
            // Simple rectangle - use 4 corners like triangle/quad for consistent handling
            pathPoints.push({ x: x1, y: y1 });
            pathPoints.push({ x: x2, y: y1 });
            pathPoints.push({ x: x2, y: y2 });
            pathPoints.push({ x: x1, y: y2 });
            pathPoints.push({ x: x1, y: y1 }); // close

            if (_DEBUG) {
              console.log("Simple rectangle path:", pathPoints);
            }
          } else {
            // Complex rectangle with corner radii - use fewer points for better zigzag handling
            const arcDetail = Math.max(3, Math.min(8, Math.ceil(Math.max(tl, tr, br, bl) / 3))); // Limit arc detail

            pathPoints.push({ x: x1 + tl, y: y1 });
            pathPoints.push({ x: x2 - tr, y: y1 });
            if (tr > 0) {
              for (let i = 1; i <= arcDetail; i++) {
                const angle = -Math.PI / 2 + (i / arcDetail) * (Math.PI / 2);
                pathPoints.push({ x: x2 - tr + Math.cos(angle) * tr, y: y1 + tr + Math.sin(angle) * tr });
              }
            }

            pathPoints.push({ x: x2, y: y1 + tr });
            pathPoints.push({ x: x2, y: y2 - br });
            if (br > 0) {
              for (let i = 1; i <= arcDetail; i++) {
                const angle = (i / arcDetail) * (Math.PI / 2);
                pathPoints.push({ x: x2 - br + Math.cos(angle) * br, y: y2 - br + Math.sin(angle) * br });
              }
            }

            pathPoints.push({ x: x2 - br, y: y2 });
            pathPoints.push({ x: x1 + bl, y: y2 });
            if (bl > 0) {
              for (let i = 1; i <= arcDetail; i++) {
                const angle = Math.PI / 2 + (i / arcDetail) * (Math.PI / 2);
                pathPoints.push({ x: x1 + bl + Math.cos(angle) * bl, y: y2 - bl + Math.sin(angle) * bl });
              }
            }

            pathPoints.push({ x: x1, y: y2 - bl });
            pathPoints.push({ x: x1, y: y1 + tl });
            if (tl > 0) {
              for (let i = 1; i <= arcDetail; i++) {
                const angle = Math.PI + (i / arcDetail) * (Math.PI / 2);
                pathPoints.push({ x: x1 + tl + Math.cos(angle) * tl, y: y1 + tl + Math.sin(angle) * tl });
              }
            }
            pathPoints.push({ x: x1 + tl, y: y1 });

            if (_DEBUG) {
              console.log("Complex rectangle path with corners:", pathPoints.length, "points, arcDetail:", arcDetail);
            }
          }

          if (_doFill) {
            let fillStitches = [];

            switch (_currentFillMode) {
              case FILL_MODE.TATAMI:
                fillStitches = createTatamiFillFromPath(pathPoints, _fillSettings);
                break;
              // Add other fill modes here as they are implemented
              default:
                fillStitches = createTatamiFillFromPath(pathPoints, _fillSettings);
            }

            if (fillStitches && fillStitches.length > 0) {
              // Add the stitches to the current thread
              _stitchData.threads[_fillThreadIndex].runs.push(fillStitches);

              // Draw fill stitches if in appropriate mode
              if (_drawMode === "stitch" || _drawMode === "realistic") {
                drawStitches(fillStitches, _fillThreadIndex);
              }
            }
          }

          if (_doStroke) {
            // Use the path-based stroke approach for consistency
            const strokeStitches = p5embroidery.convertVerticesToStitches(
              pathPoints.map((p) => ({ x: p.x, y: p.y, isVert: true })),
              _strokeSettings,
            );

            if (strokeStitches && strokeStitches.length > 0) {
              _stitchData.threads[_strokeThreadIndex].runs.push(strokeStitches);

              // Draw stroke stitches if in appropriate mode
              if (_drawMode === "stitch" || _drawMode === "realistic") {
                drawStitches(strokeStitches, _strokeThreadIndex);
              }
            }
          }

          // Handle p5 drawing mode
          if (_drawMode === "p5") {
            _originalStrokeWeightFunc.call(_p5Instance, mmToPixel(_strokeSettings.strokeWeight));
            _originalRectFunc.call(
              _p5Instance,
              mmToPixel(x),
              mmToPixel(y),
              mmToPixel(w),
              mmToPixel(h),
              ...cornerRs.map((r) => mmToPixel(r)),
            );
          }
        } else {
          _originalRectFunc.apply(this, arguments);
        }
      };
    }

    /**
     * Overrides p5.js square() function to record embroidery stitches.
     * @private
     */
    let _originalSquareFunc;
    function overrideSquareFunction() {
      _originalSquareFunc = window.square;
      window.square = function (x, y, w) {
        if (_recording) {
          window.rect.call(this, x, y, w, w);
        } else {
          _originalSquareFunc.apply(this, arguments);
        }
      };
    }

    /**
     * Overrides p5.js triangle() function to record embroidery stitches.
     * @private
     */
    let _originalTriangleFunc;
    function overrideTriangleFunction() {
      _originalTriangleFunc = window.triangle;
      window.triangle = function (x1, y1, x2, y2, x3, y3) {
        if (_recording) {
          // Build path points for triangle
          const pathPoints = [
            { x: x1, y: y1 },
            { x: x2, y: y2 },
            { x: x3, y: y3 },
            { x: x1, y: y1 }, // close
          ];
          // Fill
          if (_doFill) {
            let fillStitches = [];
            switch (_currentFillMode) {
              case FILL_MODE.TATAMI:
                fillStitches = createTatamiFillFromPath(pathPoints, _fillSettings);
                break;
              default:
                fillStitches = createTatamiFillFromPath(pathPoints, _fillSettings);
            }
            if (fillStitches && fillStitches.length > 0) {
              _stitchData.threads[_fillThreadIndex].runs.push(fillStitches);
              if (_drawMode === "stitch" || _drawMode === "realistic") {
                drawStitches(fillStitches, _fillThreadIndex);
              }
            }
          }
          // Stroke
          if (_doStroke) {
            const strokeStitches = p5embroidery.convertVerticesToStitches(
              pathPoints.map((p) => ({ x: p.x, y: p.y, isVert: true })),
              _strokeSettings,
            );
            if (strokeStitches && strokeStitches.length > 0) {
              _stitchData.threads[_strokeThreadIndex].runs.push(strokeStitches);
              if (_drawMode === "stitch" || _drawMode === "realistic") {
                drawStitches(strokeStitches, _strokeThreadIndex);
              }
            }
          }
          if (_drawMode === "p5") {
            _originalStrokeWeightFunc.call(_p5Instance, mmToPixel(_strokeSettings.strokeWeight));
            _originalTriangleFunc.call(
              _p5Instance,
              mmToPixel(x1),
              mmToPixel(y1),
              mmToPixel(x2),
              mmToPixel(y2),
              mmToPixel(x3),
              mmToPixel(y3),
            );
          }
        } else {
          _originalTriangleFunc.apply(this, arguments);
        }
      };
    }
    /**
     * Overrides p5.js quad() function to record embroidery stitches.
     * @private
     */
    let _originalQuadFunc;
    function overrideQuadFunction() {
      _originalQuadFunc = window.quad;
      window.quad = function (x1, y1, x2, y2, x3, y3, x4, y4) {
        if (_recording) {
          const pathPoints = [
            { x: x1, y: y1 },
            { x: x2, y: y2 },
            { x: x3, y: y3 },
            { x: x4, y: y4 },
            { x: x1, y: y1 }, // close
          ];
          // Fill
          if (_doFill) {
            let fillStitches = [];
            switch (_currentFillMode) {
              case FILL_MODE.TATAMI:
                fillStitches = createTatamiFillFromPath(pathPoints, _fillSettings);
                break;
              default:
                fillStitches = createTatamiFillFromPath(pathPoints, _fillSettings);
            }
            if (fillStitches && fillStitches.length > 0) {
              _stitchData.threads[_fillThreadIndex].runs.push(fillStitches);
              if (_drawMode === "stitch" || _drawMode === "realistic") {
                drawStitches(fillStitches, _fillThreadIndex);
              }
            }
          }
          // Stroke
          if (_doStroke) {
            const strokeStitches = p5embroidery.convertVerticesToStitches(
              pathPoints.map((p) => ({ x: p.x, y: p.y, isVert: true })),
              _strokeSettings,
            );
            if (strokeStitches && strokeStitches.length > 0) {
              _stitchData.threads[_strokeThreadIndex].runs.push(strokeStitches);
              if (_drawMode === "stitch" || _drawMode === "realistic") {
                drawStitches(strokeStitches, _strokeThreadIndex);
              }
            }
          }
          if (_drawMode === "p5") {
            _originalStrokeWeightFunc.call(_p5Instance, mmToPixel(_strokeSettings.strokeWeight));
            _originalQuadFunc.call(
              _p5Instance,
              mmToPixel(x1),
              mmToPixel(y1),
              mmToPixel(x2),
              mmToPixel(y2),
              mmToPixel(x3),
              mmToPixel(y3),
              mmToPixel(x4),
              mmToPixel(y4),
            );
          }
        } else {
          _originalQuadFunc.apply(this, arguments);
        }
      };
    }
    /**
     * Overrides p5.js arc() function to record embroidery stitches.
     * @private
     */
    let _originalArcFunc;
    function overrideArcFunction() {
      _originalArcFunc = window.arc;
      window.arc = function (x, y, w, h, start, stop, mode) {
        if (_recording) {
          if (_DEBUG) {
            console.log("Arc called with:", { x, y, w, h, start, stop, mode, _doFill, _doStroke });
          }

          // Default mode to OPEN if not specified
          if (mode === undefined) {
            mode = window.OPEN || "open";
          }

          // Approximate arc as polyline
          const numSteps = Math.max(
            12,
            Math.ceil((Math.abs(stop - start) * Math.max(w, h)) / (_embroiderySettings.stitchLength * 2)),
          );
          const pathPoints = [];

          // Generate arc points
          for (let i = 0; i <= numSteps; i++) {
            const theta = start + (i / numSteps) * (stop - start);
            pathPoints.push({
              x: x + (Math.cos(theta) * w) / 2,
              y: y + (Math.sin(theta) * h) / 2,
            });
          }

          if (_DEBUG) {
            console.log("Generated arc points:", pathPoints.length);
          }

          // Fill - handle all modes, not just PIE and CHORD
          if (_doFill) {
            let fillPathPoints = [...pathPoints];

            if (mode === window.PIE || mode === "pie") {
              // PIE mode: close to center and back to start
              fillPathPoints.push({ x, y });
              fillPathPoints.push(pathPoints[0]);
            } else if (mode === window.CHORD || mode === "chord") {
              // CHORD mode: close with straight line from end to start
              if (pathPoints.length > 1) {
                fillPathPoints.push(pathPoints[0]); // Close the path
              }
            } else {
              // For OPEN mode or undefined, create a pie-like fill (common expectation)
              fillPathPoints.push({ x, y });
              fillPathPoints.push(pathPoints[0]);
            }

            if (_DEBUG) {
              console.log("Fill path points:", fillPathPoints.length, "Mode:", mode);
            }

            let fillStitches = [];

            switch (_currentFillMode) {
              case FILL_MODE.TATAMI:
                fillStitches = createTatamiFillFromPath(fillPathPoints, _fillSettings);
                break;
              case FILL_MODE.SATIN:
                fillStitches = createSatinFillFromPath(fillPathPoints, _fillSettings);
                break;
              case FILL_MODE.SPIRAL:
                fillStitches = createSpiralFillFromPath(fillPathPoints, _fillSettings);
                break;
              default:
                fillStitches = createTatamiFillFromPath(fillPathPoints, _fillSettings);
            }

            if (_DEBUG) {
              console.log("Fill stitches generated:", fillStitches.length);
            }

            if (fillStitches && fillStitches.length > 0) {
              _stitchData.threads[_fillThreadIndex].runs.push(fillStitches);
              if (_drawMode === "stitch" || _drawMode === "realistic") {
                drawStitches(fillStitches, _fillThreadIndex);
              }
            }
          }

          // Stroke (uses original arc points, not closed fill path)
          if (_doStroke) {
            let strokePathPoints = pathPoints;

            // For PIE mode, include lines to center for stroke
            if (mode === window.PIE || mode === "pie") {
              strokePathPoints = [
                { x, y }, // Start at center
                ...pathPoints, // Arc points
                { x, y }, // Back to center
              ];
            } else if (mode === window.CHORD || mode === "chord") {
              // For CHORD mode, add the chord line
              strokePathPoints = [
                ...pathPoints,
                pathPoints[0], // Close with chord
              ];
            }
            // For OPEN mode, use pathPoints as-is

            const strokeStitches = p5embroidery.convertVerticesToStitches(
              strokePathPoints.map((p) => ({ x: p.x, y: p.y, isVert: true })),
              _strokeSettings,
            );

            if (strokeStitches && strokeStitches.length > 0) {
              _stitchData.threads[_strokeThreadIndex].runs.push(strokeStitches);
              if (_drawMode === "stitch" || _drawMode === "realistic") {
                drawStitches(strokeStitches, _strokeThreadIndex);
              }
            }
          }

          if (_drawMode === "p5") {
            _originalStrokeWeightFunc.call(_p5Instance, mmToPixel(_strokeSettings.strokeWeight));
            _originalArcFunc.call(_p5Instance, mmToPixel(x), mmToPixel(y), mmToPixel(w), mmToPixel(h), start, stop, mode);
          }
        } else {
          _originalArcFunc.apply(this, arguments);
        }
      };
    }

    /**
     * Overrides p5.js beginContour() function.
     * @private
     */
    let _originalBeginContourFunc;
    function overrideBeginContourFunction() {
      _originalBeginContourFunc = window.beginContour;

      window.beginContour = function () {
        if (_recording) {
          if (_DEBUG) console.log("beginContour called");
          _isContour = true;
          _currentContour = []; // Start a new contour

          if (_drawMode === "p5") {
            _originalBeginContourFunc.call(_p5Instance);
          }
        } else {
          _originalBeginContourFunc.apply(this, arguments);
        }
      };
    }

    /**
     * Overrides p5.js endContour() function.
     * @private
     */
    let _originalEndContourFunc;
    function overrideEndContourFunction() {
      _originalEndContourFunc = window.endContour;

      window.endContour = function () {
        if (_recording) {
          if (_DEBUG) console.log("endContour called, current contour length:", _currentContour.length);

          if (_currentContour.length > 0) {
            // Close the contour if it's not already closed
            const firstPoint = _currentContour[0];
            const lastPoint = _currentContour[_currentContour.length - 1];
            const distance = Math.sqrt(Math.pow(lastPoint.x - firstPoint.x, 2) + Math.pow(lastPoint.y - firstPoint.y, 2));

            // If the contour isn't closed, close it
            if (distance > 0.1) {
              _currentContour.push({ x: firstPoint.x, y: firstPoint.y });
            }

            // Add the completed contour to the contours array
            _contours.push([..._currentContour]);
            if (_DEBUG) console.log("Added contour with", _currentContour.length, "points");
          }

          _currentContour = [];
          _isContour = false;

          if (_drawMode === "p5") {
            _originalEndContourFunc.call(_p5Instance);
          }
        } else {
          _originalEndContourFunc.apply(this, arguments);
        }
      };
    }

    /**
     * Overrides necessary p5.js functions for embroidery recording.
     * @private
     */
    function overrideP5Functions() {
      overrideLineFunction();
      overrideCurveFunction();
      overrideBezierFunction();
      overrideEllipseFunction();
      overrideCircleFunction();
      overrideStrokeWeightFunction();
      overrideStrokeJoinFunction();
      overridePointFunction();
      overrideStrokeFunction();
      overrideNoStrokeFunction();
      overrideFillFunction();
      overrideNoFillFunction();
      overrideRectFunction();
      overrideSquareFunction();
      overrideTriangleFunction();
      overrideQuadFunction();
      overrideArcFunction();
      overrideVertexFunction();
      overrideBezierVertexFunction();
      overrideQuadraticVertexFunction();
      overrideCurveVertexFunction();
      overrideBeginShapeFunction();
      overrideEndShapeFunction();
      overrideBeginContourFunction();
      overrideEndContourFunction();
      // Add more overrides as needed
    }

    /**
     * Restores original p5.js functions.
     * @private
     */
    function restoreP5Functions() {
      window.line = _originalLineFunc;
      window.curve = _originalCurveFunc;
      window.bezier = _originalBezierFunc;
      window.ellipse = _originalEllipseFunc;
      window.strokeWeight = _originalStrokeWeightFunc;
      window.strokeJoin = _originalStrokeJoinFunc;
      window.point = _originalPointFunc;
      window.stroke = _originalStrokeFunc;
      window.noStroke = _originalNoStrokeFunc;
      window.fill = _originalFillFunc;
      window.noFill = _originalNoFillFunc;
      window.rect = _originalRectFunc;
      window.square = _originalSquareFunc;
      window.triangle = _originalTriangleFunc;
      window.quad = _originalQuadFunc;
      window.arc = _originalArcFunc;
      window.vertex = _originalVertexFunc;
      window.bezierVertex = _originalBezierVertexFunc;
      window.quadraticVertex = _originalQuadraticVertexFunc;
      window.curveVertex = _originalCurveVertexFunc;
      window.beginShape = _originalBeginShapeFunc;
      window.endShape = _originalEndShapeFunc;
      window.beginContour = _originalBeginContourFunc;
      window.endContour = _originalEndContourFunc;
      // Restore other functions as needed
    }

    /**
     * Sets the stitch parameters for embroidery.
     * @method setStitch
     * @for p5
     * @param {Number} minLength - Minimum stitch length in millimeters
     * @param {Number} desiredLength - Desired stitch length in millimeters
     * @param {Number} noise - Amount of random variation in stitch length (0-1)
     * @example
     *
     *
     * function setup() {
     *   createCanvas(400, 400);
     *   beginRecord(this);
     *   setStitch(1, 3, 0.2); // min 1mm, desired 3mm, 20% noise
     *   // Draw embroidery patterns
     * }
     *
     *
     */
    p5embroidery.setStitch = function (minLength, desiredLength, noise) {
      _embroiderySettings.minStitchLength = Math.max(0, minLength);
      _embroiderySettings.stitchLength = Math.max(0.1, desiredLength);
      _embroiderySettings.resampleNoise = Math.min(1, Math.max(0, noise));

      _strokeSettings.minStitchLength = _embroiderySettings.minStitchLength;
      _strokeSettings.stitchLength = _embroiderySettings.stitchLength;
      _strokeSettings.resampleNoise = _embroiderySettings.resampleNoise;
    };

    /**
     * Sets the stroke settings for embroidery.
     * @method setStrokeSettings
     * @for p5
     * @param {Object} settings - The settings for the stroke
     */

    p5embroidery.setStrokeSettings = function (settings) {
      // Merge default settings with provided settings
      Object.assign(_strokeSettings, settings);
    };

    /**
     * Sets the fill settings for embroidery.
     * @method setFillSettings
     * @for p5
     * @param {Object} settings - The settings for the fill
     */

    p5embroidery.setFillSettings = function (settings) {
      // Merge default settings with provided settings
      Object.assign(_fillSettings, settings);
    };

    /**
     * Sets the draw mode for embroidery.
     * @method setDrawMode
     * @for p5
     * @param {String} mode - The draw mode to set ('stitch', 'p5', 'realistic')
     * @example
     *
     *
     * function setup() {
     *   createCanvas(400, 400);
     *   beginRecord(this);
     *   setDrawMode('stitch'); // Show stitch points and lines
     *   // Draw embroidery patterns
     * }
     *
     *
     */
    p5embroidery.setDrawMode = function (mode) {
      _drawMode = mode;
    };

    /**
     * Converts a line segment into a series of stitches.
     * @private
     * @param {number} x1 - Starting x-coordinate in mm
     * @param {number} y1 - Starting y-coordinate in mm
     * @param {number} x2 - Ending x-coordinate in mm
     * @param {number} y2 - Ending y-coordinate in mm
     * @param {Object} stitchSettings - Settings for the stitches
     * @returns {Array<{x: number, y: number}>} Array of stitch points in mm
     */
    function convertLineToStitches(x1, y1, x2, y2, stitchSettings = _embroiderySettings) {
      if (_DEBUG)
        console.log("Converting line to stitches (before offset):", {
          from: { x: x1, y: y1 },
          to: { x: x2, y: y2 },
        });

      if (_DEBUG)
        console.log("Converting line to stitches", {
          from: { x: x1, y: y1 },
          to: { x: x2, y: y2 },
        });

      let dx = x2 - x1;
      let dy = y2 - y1;
      let distance = Math.sqrt(dx * dx + dy * dy);

      if (_DEBUG)
        console.log("Line properties:", {
          dx,
          dy,
          distance,
          minStitchLength: stitchSettings.minStitchLength,
          stitchLength: stitchSettings.stitchLength,
          strokeWeight: stitchSettings.strokeWeight,
        });

      if (stitchSettings.strokeWeight > 0) {
        switch (_currentStrokeMode) {
          case STROKE_MODE.STRAIGHT:
            return straightLineStitch(x1, y1, x2, y2, stitchSettings);
          case STROKE_MODE.ZIGZAG:
            return zigzagStitch(x1, y1, x2, y2, stitchSettings);
          case STROKE_MODE.LINES:
            return multiLineStitch(x1, y1, x2, y2, stitchSettings);
          case STROKE_MODE.SASHIKO:
            return sashikoStitch(x1, y1, x2, y2, stitchSettings);
          default:
            return straightLineStitch(x1, y1, x2, y2, stitchSettings);
        }
      } else {
        return straightLineStitch(x1, y1, x2, y2, stitchSettings);
      }
    }

    /**
     * Converts a path into a series of stitches with proper corner joins.
     * @private
     * @param {Array<{x: number, y: number}>} pathPoints - Array of path points in mm
     * @param {Object} stitchSettings - Settings for the stitches
     * @returns {Array<{x: number, y: number}>} Array of stitch points in mm
     */
    function convertPathToStitchesWithJoins(pathPoints, stitchSettings = _embroiderySettings) {
      if (!pathPoints || pathPoints.length < 2) {
        console.warn("Cannot convert path to stitches from insufficient path points");
        return [];
      }

      const strokeWeight = stitchSettings.strokeWeight;
      const joinType = stitchSettings.strokeJoin || _currentStrokeJoin;

      if (_DEBUG) {
        console.log("convertPathToStitchesWithJoins:", {
          pathPointsCount: pathPoints.length,
          strokeWeight: strokeWeight,
          strokeMode: stitchSettings.strokeMode,
          joinType: joinType,
        });
      }

      // For zigzag and other wide stroke modes, use a different approach
      switch (stitchSettings.strokeMode) {
        case STROKE_MODE.ZIGZAG:
          if (_DEBUG) console.log("Creating zigzag with joins...");
          const zigzagResult = createZigzagWithJoins(pathPoints, stitchSettings);
          if (_DEBUG) console.log("Zigzag result:", zigzagResult.length, "stitches");
          return zigzagResult;
        case STROKE_MODE.LINES:
          return multiLineStitchFromPath(pathPoints, stitchSettings);
        case STROKE_MODE.SASHIKO:
          return sashikoStitchFromPath(pathPoints, stitchSettings);
        default:
          return straightLineStitchFromPath(pathPoints, stitchSettings);
      }
    }

    /**
     * Creates a continuous zigzag pattern with proper corner joins.
     * @private
     * @param {Array<{x: number, y: number}>} pathPoints - Array of path points in mm
     * @param {Object} stitchSettings - Settings for the stitches
     * @returns {Array<{x: number, y: number}>} Array of stitch points in mm
     */
    function createZigzagWithJoins(pathPoints, stitchSettings) {
      if (!pathPoints || pathPoints.length < 2) {
        return [];
      }

      const width = stitchSettings.strokeWeight;
      const density = stitchSettings.stitchLength;
      const result = [];

      // Create parallel paths offset to each side
      const leftPath = createOffsetPath(pathPoints, width / 2, true);
      const rightPath = createOffsetPath(pathPoints, width / 2, false);

      if (leftPath.length === 0 || rightPath.length === 0) {
        // Fall back to simple zigzag if offset calculation fails
        return zigzagStitchFromPath(pathPoints, stitchSettings);
      }

      if (_DEBUG) {
        console.log("Original path:", pathPoints);
        console.log("Left path:", leftPath);
        console.log("Right path:", rightPath);
      }

      // Calculate total path length for zigzag spacing
      let totalLength = 0;
      for (let i = 0; i < pathPoints.length - 1; i++) {
        const dx = pathPoints[i + 1].x - pathPoints[i].x;
        const dy = pathPoints[i + 1].y - pathPoints[i].y;
        totalLength += Math.sqrt(dx * dx + dy * dy);
      }

      const numZigzags = Math.max(2, Math.floor(totalLength / density));

      // Start from left side and ensure we always get valid points
      let currentSide = leftPath;
      let isOnLeftSide = true;

      // Interpolate between left and right paths to create zigzag
      for (let i = 0; i <= numZigzags; i++) {
        const t = i / numZigzags;

        // Alternate between sides for each stitch
        currentSide = isOnLeftSide ? leftPath : rightPath;
        const point = getPointAtRatio(currentSide, t);

        if (point) {
          result.push(point);
          if (_DEBUG && i < 5) {
            console.log(`Zigzag point ${i}: side=${isOnLeftSide ? "left" : "right"}, t=${t}, point=`, point);
          }
        } else {
          // If we can't get a point from one side, try the other
          const alternateSide = isOnLeftSide ? rightPath : leftPath;
          const alternatePoint = getPointAtRatio(alternateSide, t);
          if (alternatePoint) {
            result.push(alternatePoint);
          }
        }

        // Toggle sides for next iteration
        isOnLeftSide = !isOnLeftSide;
      }

      if (_DEBUG) {
        console.log("Zigzag result:", result.slice(0, 10));
      }

      return result;
    }

    /**
     * Simplifies a path by removing redundant points that are too close together.
     * @private
     */
    function simplifyPath(pathPoints, tolerance = 0.1) {
      if (pathPoints.length <= 2) return pathPoints;

      const simplified = [pathPoints[0]]; // Always keep first point

      for (let i = 1; i < pathPoints.length - 1; i++) {
        const prev = simplified[simplified.length - 1];
        const curr = pathPoints[i];
        pathPoints[i + 1];

        // Calculate distance from current point to previous
        const distToPrev = Math.sqrt((curr.x - prev.x) * (curr.x - prev.x) + (curr.y - prev.y) * (curr.y - prev.y));

        // Keep point if it's far enough from previous point
        if (distToPrev >= tolerance) {
          simplified.push(curr);
        }
      }

      // Always keep last point
      simplified.push(pathPoints[pathPoints.length - 1]);

      if (_DEBUG && simplified.length !== pathPoints.length) {
        console.log(`Path simplified: ${pathPoints.length} -> ${simplified.length} points`);
      }

      return simplified;
    }

    /**
     * Creates an offset path parallel to the original path.
     * @private
     * @param {Array<{x: number, y: number}>} pathPoints - Original path points
     * @param {number} offset - Offset distance (positive = left, negative = right)
     * @param {boolean} isLeft - Whether this is the left side offset
     * @returns {Array<{x: number, y: number}>} Offset path points
     */
    function createOffsetPath(pathPoints, offset, isLeft) {
      const offsetPath = [];

      // Simplify the path first to remove closely spaced points
      const simplifiedPath = simplifyPath(pathPoints, 0.2);

      // Check if this is a closed path (first and last points are the same or very close)
      const isClosedPath =
        simplifiedPath.length > 2 &&
        Math.abs(simplifiedPath[0].x - simplifiedPath[simplifiedPath.length - 1].x) < 0.1 &&
        Math.abs(simplifiedPath[0].y - simplifiedPath[simplifiedPath.length - 1].y) < 0.1;

      for (let i = 0; i < simplifiedPath.length; i++) {
        const curr = simplifiedPath[i];
        let prev, next;

        if (isClosedPath) {
          // For closed paths, wrap around for prev/next calculation
          prev = i > 0 ? simplifiedPath[i - 1] : simplifiedPath[simplifiedPath.length - 2]; // Skip duplicate end point
          next = i < simplifiedPath.length - 1 ? simplifiedPath[i + 1] : simplifiedPath[1]; // Skip duplicate start point
        } else {
          // For open paths, use normal indexing
          prev = i > 0 ? simplifiedPath[i - 1] : null;
          next = i < simplifiedPath.length - 1 ? simplifiedPath[i + 1] : null;
        }

        let offsetPoint;

        if (prev === null) {
          // First point of open path - use direction to next point
          const dx = next.x - curr.x;
          const dy = next.y - curr.y;
          const len = Math.sqrt(dx * dx + dy * dy);
          if (len > 0) {
            const perpX = (-dy / len) * (isLeft ? offset : -offset);
            const perpY = (dx / len) * (isLeft ? offset : -offset);
            offsetPoint = { x: curr.x + perpX, y: curr.y + perpY };
          } else {
            offsetPoint = { x: curr.x, y: curr.y };
          }
        } else if (next === null) {
          // Last point of open path - use direction from previous point
          const dx = curr.x - prev.x;
          const dy = curr.y - prev.y;
          const len = Math.sqrt(dx * dx + dy * dy);
          if (len > 0) {
            const perpX = (-dy / len) * (isLeft ? offset : -offset);
            const perpY = (dx / len) * (isLeft ? offset : -offset);
            offsetPoint = { x: curr.x + perpX, y: curr.y + perpY };
          } else {
            offsetPoint = { x: curr.x, y: curr.y };
          }
        } else {
          // Middle point or closed path vertex - calculate proper join
          offsetPoint = calculateOffsetCorner(prev, curr, next, offset, isLeft);
        }

        offsetPath.push(offsetPoint);
      }

      return offsetPath;
    }

    /**
     * Calculates the offset corner point with proper join handling.
     * @private
     */
    function calculateOffsetCorner(p1, p2, p3, offset, isLeft) {
      // Calculate direction vectors
      const v1 = { x: p2.x - p1.x, y: p2.y - p1.y };
      const v2 = { x: p3.x - p2.x, y: p3.y - p2.y };

      // Normalize
      const len1 = Math.sqrt(v1.x * v1.x + v1.y * v1.y);
      const len2 = Math.sqrt(v2.x * v2.x + v2.y * v2.y);

      // Handle very short segments (common in curved corners)
      if (len1 < 0.1 || len2 < 0.1) {
        // Use simple perpendicular offset for tiny segments
        const avgVecX = (v1.x + v2.x) / 2;
        const avgVecY = (v1.y + v2.y) / 2;
        const avgLen = Math.sqrt(avgVecX * avgVecX + avgVecY * avgVecY);

        if (avgLen > 0) {
          const actualOffset = isLeft ? offset : -offset;
          const perpX = (-avgVecY / avgLen) * actualOffset;
          const perpY = (avgVecX / avgLen) * actualOffset;
          return { x: p2.x + perpX, y: p2.y + perpY };
        }
        return { x: p2.x, y: p2.y };
      }

      const n1 = { x: v1.x / len1, y: v1.y / len1 };
      const n2 = { x: v2.x / len2, y: v2.y / len2 };

      // Check angle between vectors to handle sharp turns
      const dot = n1.x * n2.x + n1.y * n2.y;
      const angle = Math.acos(Math.max(-1, Math.min(1, dot)));

      // Calculate perpendiculars
      const actualOffset = isLeft ? offset : -offset;
      const perp1 = { x: -n1.y * actualOffset, y: n1.x * actualOffset };
      const perp2 = { x: -n2.y * actualOffset, y: n2.x * actualOffset };

      // For very small angles (nearly straight line), use simple averaging
      if (angle < 0.1) {
        const avgPerpX = (perp1.x + perp2.x) / 2;
        const avgPerpY = (perp1.y + perp2.y) / 2;
        return { x: p2.x + avgPerpX, y: p2.y + avgPerpY };
      }

      // For sharp angles (< 30 degrees), limit the miter to prevent extreme spikes
      if (angle < Math.PI / 6) {
        const avgPerpX = (perp1.x + perp2.x) / 2;
        const avgPerpY = (perp1.y + perp2.y) / 2;
        return { x: p2.x + avgPerpX, y: p2.y + avgPerpY };
      }

      // Calculate intersection of offset lines
      const line1Start = { x: p1.x + perp1.x, y: p1.y + perp1.y };
      const line1End = { x: p2.x + perp1.x, y: p2.y + perp1.y };
      const line2Start = { x: p2.x + perp2.x, y: p2.y + perp2.y };
      const line2End = { x: p3.x + perp2.x, y: p3.y + perp2.y };

      const intersection = lineLineIntersection(line1Start, line1End, line2Start, line2End);

      if (intersection) {
        // Limit miter length to prevent extreme spikes
        const miterDistance = Math.sqrt(
          (intersection.x - p2.x) * (intersection.x - p2.x) + (intersection.y - p2.y) * (intersection.y - p2.y),
        );
        const maxMiterDistance = Math.abs(offset) * 3; // Reduced miter limit for stability

        if (miterDistance <= maxMiterDistance) {
          return intersection;
        }
      }

      // Fall back to averaged perpendicular for failed intersections or extreme miters
      const avgPerpX = (perp1.x + perp2.x) / 2;
      const avgPerpY = (perp1.y + perp2.y) / 2;
      const avgLen = Math.sqrt(avgPerpX * avgPerpX + avgPerpY * avgPerpY);

      if (avgLen > 0) {
        // Normalize and scale to proper offset
        const scale = Math.abs(offset) / avgLen;
        return {
          x: p2.x + avgPerpX * scale,
          y: p2.y + avgPerpY * scale,
        };
      } else {
        return { x: p2.x, y: p2.y };
      }
    }

    /**
     * Gets a point at a specific ratio along a path.
     * @private
     */
    function getPointAtRatio(pathPoints, ratio) {
      if (!pathPoints || pathPoints.length === 0) return null;
      if (pathPoints.length === 1) return pathPoints[0];

      // Calculate total path length
      let totalLength = 0;
      const segments = [];

      for (let i = 0; i < pathPoints.length - 1; i++) {
        const dx = pathPoints[i + 1].x - pathPoints[i].x;
        const dy = pathPoints[i + 1].y - pathPoints[i].y;
        const length = Math.sqrt(dx * dx + dy * dy);
        segments.push(length);
        totalLength += length;
      }

      if (totalLength === 0) return pathPoints[0];

      // Find target distance along path
      const targetDistance = ratio * totalLength;
      let currentDistance = 0;

      // Find which segment contains the target point
      for (let i = 0; i < segments.length; i++) {
        if (currentDistance + segments[i] >= targetDistance) {
          // Interpolate within this segment
          const segmentRatio = (targetDistance - currentDistance) / segments[i];
          const p1 = pathPoints[i];
          const p2 = pathPoints[i + 1];

          return {
            x: p1.x + (p2.x - p1.x) * segmentRatio,
            y: p1.y + (p2.y - p1.y) * segmentRatio,
          };
        }
        currentDistance += segments[i];
      }

      // If we get here, return the last point
      return pathPoints[pathPoints.length - 1];
    }

    /**
     * Converts a path into a series of stitches.
     * @private
     * @param {Array<{x: number, y: number}>} pathPoints - Array of path points in mm
     * @param {Object} stitchSettings - Settings for the stitches
     * @returns {Array<{x: number, y: number}>} Array of stitch points in mm
     */
    function convertPathToStitches(pathPoints, stitchSettings = _embroiderySettings) {
      if (!pathPoints || pathPoints.length < 2) {
        console.warn("Cannot convert path to stitches from insufficient path points");
        return [];
      }

      if (stitchSettings.strokeWeight > 0) {
        switch (_currentStrokeMode) {
          case STROKE_MODE.STRAIGHT:
            return straightLineStitchFromPath(pathPoints, stitchSettings);
          case STROKE_MODE.ZIGZAG:
            return zigzagStitchFromPath(pathPoints, stitchSettings);
          case STROKE_MODE.LINES:
            return multiLineStitchFromPath(pathPoints, stitchSettings);
          case STROKE_MODE.SASHIKO:
            return sashikoStitchFromPath(pathPoints, stitchSettings);
          default:
            // For simple straight stitches, we'll need to break this down segment by segment
            const result = [];
            for (let i = 0; i < pathPoints.length - 1; i++) {
              const p1 = pathPoints[i];
              const p2 = pathPoints[i + 1];
              const segmentStitches = straightLineStitch(p1.x, p1.y, p2.x, p2.y, stitchSettings);
              result.push(...segmentStitches);
            }
            return result;
        }
      } else {
        // For simple straight stitches, we'll need to break this down segment by segment
        const result = [];
        for (let i = 0; i < pathPoints.length - 1; i++) {
          const p1 = pathPoints[i];
          const p2 = pathPoints[i + 1];
          const segmentStitches = straightLineStitch(p1.x, p1.y, p2.x, p2.y, stitchSettings);
          result.push(...segmentStitches);
        }
        return result;
      }
    }

    /**
     * Creates zigzag stitches.
     * @method zigzagStitch
     * @private
     * @param {number} x1 - Starting x-coordinate in mm
     * @param {number} y1 - Starting y-coordinate in mm
     * @param {number} x2 - Ending x-coordinate in mm
     * @param {number} y2 - Ending y-coordinate in mm
     * @param {Object} stitchSettings - Settings for the stitches
     * @returns {Array<{x: number, y: number}>} Array of stitch points in mm
     */
    function zigzagStitch(x1, y1, x2, y2, stitchSettings) {

      // For a simple straight line, we can implement the zigzag directly
      // instead of calling the path-based version
      let stitches = [];
      let dx = x2 - x1;
      let dy = y2 - y1;
      let distance = Math.sqrt(dx * dx + dy * dy);

      // Check for zero distance to prevent division by zero
      if (distance === 0 || distance < 0.001) {
        // If points are the same or very close, just return the start point
        if (_DEBUG) console.log("Zero distance detected in zigzagStitch, returning single point");
        return [
          {
            x: x1,
            y: y1,
          },
        ];
      }

      // Calculate perpendicular vector for zigzag
      let perpX = -dy / distance;
      let perpY = dx / distance;

      // Use strokeWeight for the width of the zigzag
      let width = stitchSettings.strokeWeight > 0 ? stitchSettings.strokeWeight : 2;

      // Calculate number of zigzag segments
      let zigzagDistance = stitchSettings.stitchLength;
      let numZigzags = Math.max(2, Math.floor(distance / zigzagDistance));

      // Create zigzag pattern
      let halfWidth = width / 2;
      let side = 1; // Start with one side

      // Add first point
      stitches.push({
        x: x1 + perpX * halfWidth * side,
        y: y1 + perpY * halfWidth * side,
      });

      // Add zigzag points
      for (let i = 1; i <= numZigzags; i++) {
        let t = i / numZigzags;
        side = -side; // Alternate sides

        let pointX = x1 + dx * t + perpX * halfWidth * side;
        let pointY = y1 + dy * t + perpY * halfWidth * side;

        stitches.push({
          x: pointX,
          y: pointY,
        });
      }

      // Make sure we end at the endpoint
      if (side !== -1) {
        // If we didn't end on the opposite side
        stitches.push({
          x: x2 + perpX * halfWidth * -1, // End on opposite side
          y: y2 + perpY * halfWidth * -1,
        });
      }

      if (_DEBUG) console.log("Generated zigzag stitches:", stitches);
      return stitches;
    }

    /**
     * Creates straight line stitches.
     * @method straightLineStitch
     * @private
     * @param {number} x1 - Starting x-coordinate in mm
     * @param {number} y1 - Starting y-coordinate in mm
     * @param {number} x2 - Ending x-coordinate in mm
     * @param {number} y2 - Ending y-coordinate in mm
     * @param {Object} [stitchSettings=_embroiderySettings] - Settings for the stitches
     * @returns {Array<{x: number, y: number}>} Array of stitch points in mm
     */
    function straightLineStitch(x1, y1, x2, y2, stitchSettings = _embroiderySettings) {
      let stitches = [];
      let dx = x2 - x1;
      let dy = y2 - y1;
      const distance = Math.sqrt(dx * dx + dy * dy);
      // Add first stitch at starting point
      stitches.push({
        x: x1,
        y: y1,
      });

      // If distance is less than minimum stitch length, we're done
      if (distance < stitchSettings.minStitchLength) {
        return stitches;
      }

      let baseStitchLength = stitchSettings.stitchLength;
      let numStitches = Math.floor(distance / baseStitchLength);
      let currentDistance = 0;

      //console.log("numStitches",numStitches)
      // Handle full-length stitches
      for (let i = 0; i < numStitches; i++) {
        // Add noise to stitch length if specified
        let stitchLength = baseStitchLength;
        if (stitchSettings.resampleNoise > 0) {
          let noise = (Math.random() * 2 - 1) * stitchSettings.resampleNoise;
          stitchLength *= 1 + noise;
        }

        // update cumulative distance
        currentDistance += stitchLength;
        let t = Math.min(currentDistance / distance, 1);
        //console.log("t",t)
        stitches.push({
          x: x1 + dx * t,
          y: y1 + dy * t,
        });
      }

      // Add final stitch at end point if needed
      let remainingDistance = distance - currentDistance;
      if (remainingDistance > stitchSettings.minStitchLength || numStitches === 0) {
        stitches.push({
          x: x2,
          y: y2,
        });
      }

      if (_DEBUG) console.log("Generated straight line stitches:", stitches);
      return stitches;
    }

    /**
     * Creates line zigzag stitches that takes an array of path points
     * @private
     * @param {Array<{x: number, y: number}>} pathPoints - Array of path points in mm
     * @param {Object} stitchSettings - Settings for the stitches
     * @returns {Array<{x: number, y: number}>} Array of stitch points in mm
     */
    function zigzagStitchFromPath(pathPoints, stitchSettings) {
      if (!pathPoints || pathPoints.length < 2) {
        console.warn("Cannot create zigzag stitching from insufficient path points");
        return [];
      }

      const result = [];
      stitchSettings.strokeWeight > 0 ? stitchSettings.strokeWeight : 2;

      // Process each segment between consecutive points
      for (let i = 0; i < pathPoints.length - 1; i++) {
        const p1 = pathPoints[i];
        const p2 = pathPoints[i + 1];

        // Get zigzag stitches for this segment
        const segmentStitches = zigzagStitch(p1.x, p1.y, p2.x, p2.y, stitchSettings);
        result.push(...segmentStitches);
      }

      return result;
    }

    function multiLineStitch(x1, y1, x2, y2, stitchSettings) {
      // This is now a wrapper function that calls the path-based implementation
      const pathPoints = [
        { x: x1, y: y1 },
        { x: x2, y: y2 },
      ];
      return multiLineStitchFromPath(pathPoints, stitchSettings);
    }

    /**
     * Creates multi-line stitches from an array of stitch points
     * @private
     * @param {Array<{x: number, y: number}>} pathPoints - Array of path points in mm
     * @param {Object} stitchSettings - Settings for the stitches
     * @returns {Array<{x: number, y: number}>} Array of stitch points in mm
     */
    function multiLineStitchFromPath(pathPoints, stitchSettings) {
      if (!pathPoints || pathPoints.length < 2) {
        console.warn("Cannot create multi-line stitching from insufficient path points");
        return [];
      }

      const threadWeight = stitchSettings.stitchWidth || 0.2;
      const width = stitchSettings.strokeWeight || 2;
      const numLines = Math.max(2, Math.floor(width / threadWeight));
      const result = [];

      // Calculate the spacing between lines
      const spacing = width / (numLines - 1);

      // Generate multiple parallel paths
      for (let i = 0; i < numLines; i++) {
        // Calculate offset from center
        const offset = i * spacing - width / 2;
        const offsetPath = [];

        // Calculate perpendicular vectors for each segment and apply offset
        for (let j = 0; j < pathPoints.length; j++) {
          // For first point or when calculating new perpendicular
          if (j === 0 || j === pathPoints.length - 1) {
            let perpX, perpY;

            if (j === 0) {
              // For first point, use direction to next point
              const dx = pathPoints[1].x - pathPoints[0].x;
              const dy = pathPoints[1].y - pathPoints[0].y;
              const distance = Math.sqrt(dx * dx + dy * dy);
              if (distance === 0 || distance < 0.001) {
                // Skip this point if distance is zero
                continue;
              }
              perpX = -dy / distance;
              perpY = dx / distance;
            } else {
              // For last point, use direction from previous point
              const dx = pathPoints[j].x - pathPoints[j - 1].x;
              const dy = pathPoints[j].y - pathPoints[j - 1].y;
              const distance = Math.sqrt(dx * dx + dy * dy);
              if (distance === 0 || distance < 0.001) {
                // Skip this point if distance is zero
                continue;
              }
              perpX = -dy / distance;
              perpY = dx / distance;
            }

            offsetPath.push({
              x: pathPoints[j].x + perpX * offset,
              y: pathPoints[j].y + perpY * offset,
            });
          } else {
            // For interior points, average the perpendiculars of adjacent segments
            const prevDx = pathPoints[j].x - pathPoints[j - 1].x;
            const prevDy = pathPoints[j].y - pathPoints[j - 1].y;
            const prevDistance = Math.sqrt(prevDx * prevDx + prevDy * prevDy);

            const nextDx = pathPoints[j + 1].x - pathPoints[j].x;
            const nextDy = pathPoints[j + 1].y - pathPoints[j].y;
            const nextDistance = Math.sqrt(nextDx * nextDx + nextDy * nextDy);

            // Calculate perpendicular vectors
            const prevPerpX = -prevDy / prevDistance;
            const prevPerpY = prevDx / prevDistance;

            const nextPerpX = -nextDy / nextDistance;
            const nextPerpY = nextDx / nextDistance;

            // Average the perpendicular vectors
            const perpX = (prevPerpX + nextPerpX) / 2;
            const perpY = (prevPerpY + nextPerpY) / 2;

            // Normalize the averaged vector
            const length = Math.sqrt(perpX * perpX + perpY * perpY);

            offsetPath.push({
              x: pathPoints[j].x + (perpX / length) * offset,
              y: pathPoints[j].y + (perpY / length) * offset,
            });
          }
        }

        // For even lines, go from start to end
        // For odd lines, go from end to start (back and forth pattern)
        if (i % 2 === 0) {
          for (let j = 0; j < offsetPath.length - 1; j++) {
            const start = offsetPath[j];
            const end = offsetPath[j + 1];
            const lineStitches = straightLineStitch(start.x, start.y, end.x, end.y, stitchSettings);
            result.push(...lineStitches);
          }
        } else {
          for (let j = offsetPath.length - 1; j > 0; j--) {
            const start = offsetPath[j];
            const end = offsetPath[j - 1];
            const lineStitches = straightLineStitch(start.x, start.y, end.x, end.y, stitchSettings);
            result.push(...lineStitches);
          }
        }
      }

      return result;
    }

    function sashikoStitch(x1, y1, x2, y2, stitchSettings) {
      // This is now a wrapper function that calls the path-based implementation
      const pathPoints = [
        { x: x1, y: y1 },
        { x: x2, y: y2 },
      ];
      return sashikoStitchFromPath(pathPoints, stitchSettings);
    }

    /**
     * Creates sashiko stitches from an array of path points
     * @private
     * @param {Array<{x: number, y: number}>} pathPoints - Array of path points in mm
     * @param {Object} stitchSettings - Settings for the stitches
     * @returns {Array<{x: number, y: number}>} Array of stitch points in mm
     */
    function sashikoStitchFromPath(pathPoints, stitchSettings) {
      if (!pathPoints || pathPoints.length < 2) {
        console.warn("Cannot create sashiko stitching from insufficient path points");
        return [];
      }

      const result = [];

      // Process each segment between consecutive points
      for (let i = 0; i < pathPoints.length - 1; i++) {
        const p1 = pathPoints[i];
        const p2 = pathPoints[i + 1];

        // Calculate direction and distance
        const dx = p2.x - p1.x;
        const dy = p2.y - p1.y;
        const distance = Math.sqrt(dx * dx + dy * dy);

        // Normalize direction vector
        const dirX = dx / distance;
        const dirY = dy / distance;

        // Sashiko stitch length (longer than regular stitches)
        const sashikoStitchLength = stitchSettings.stitchLength * 2;
        const multilineLength = sashikoStitchLength * 0.5;
        const straightLength = sashikoStitchLength * 0.5;

        let currentDist = 0;
        let isMultiline = true;

        // Create segments along this path section
        while (currentDist < distance) {
          const segmentLength = isMultiline ? multilineLength : straightLength;
          const endDist = Math.min(currentDist + segmentLength, distance);

          // Calculate segment start and end points
          const segStartX = p1.x + dirX * currentDist;
          const segStartY = p1.y + dirY * currentDist;
          const segEndX = p1.x + dirX * endDist;
          const segEndY = p1.y + dirY * endDist;

          // Create segment pathPoints
          const segmentPoints = [
            { x: segStartX, y: segStartY },
            { x: segEndX, y: segEndY },
          ];

          if (isMultiline) {
            // Use the pathPoints version of multiLine stitching
            const lineStitches = multiLineStitchFromPath(segmentPoints, stitchSettings);
            result.push(...lineStitches);
          } else {
            // Create single straight line for this segment
            const lineStitches = straightLineStitch(segStartX, segStartY, segEndX, segEndY, stitchSettings);
            result.push(...lineStitches);
          }

          // Move to next segment
          currentDist = endDist;
          isMultiline = !isMultiline; // Toggle between multiline and straight line
        }
      }

      return result;
    }

    /**
     * Exports the recorded embroidery data as a file.
     * @method exportEmbroidery
     * @for p5
     * @param {String} filename - Output filename with extension
     * @example
     *
     *
     * function setup() {
     *   createCanvas(400, 400);
     *   beginRecord(this);
     *   // Draw embroidery patterns here
     *   endRecord();
     *   exportEmbroidery('pattern.dst');
     * }
     *
     *
     */
    p5embroidery.exportEmbroidery = function (filename) {
      const extension = filename.split(".").pop().toLowerCase();

      switch (extension) {
        case "dst":
          p5embroidery.exportDST(filename);
          break;
        default:
          console.error(`Unsupported embroidery format: ${extension}`);
          break;
      }
    };

    /**
     * Exports the recorded embroidery data as a G-code file.
     * @method exportGcode
     * @for p5
     * @param {String} filename - Output filename
     * @example
     *
     *
     * function setup() {
     *   createCanvas(400, 400);
     *   beginRecord(this);
     *   // Draw embroidery patterns
     *   endRecord();
     *   exportGcode('pattern.gcode');
     * }
     *
     *
     */
    p5embroidery.exportGcode = function (filename) {
      const points = [];
      for (const thread of _stitchData.threads) {
        for (const run of thread.runs) {
          for (const stitch of run) {
            points.push({
              x: stitch.x,
              y: stitch.y,
              command: stitch.command,
            });
          }
        }
      }

      const gcodeWriter = new GCodeWriter();
      gcodeWriter.addComment("Embroidery Pattern");
      if (points.length > 0) {
        gcodeWriter.move(points[0].x, points[0].y);
        for (const point of points) {
          gcodeWriter.move(point.x, point.y);
        }
      }
      gcodeWriter.saveGcode(points, "EmbroideryPattern", filename);
    };

    /**
     * Exports the recorded embroidery data as a DST file.
     * @method exportDST
     * @for p5
     * @param {String} [filename='embroideryPattern.dst'] - Output filename
     * @example
     *
     *
     * function setup() {
     *   createCanvas(400, 400);
     *   beginRecord(this);
     *   // Draw embroidery patterns
     *   endRecord();
     *   exportDST('pattern.dst');
     * }
     *
     *
     */
    p5embroidery.exportDST = function (filename = "embroideryPattern.dst") {
      const points = [];
      const dstWriter = new DSTWriter();

      if (_DEBUG) console.log("=== Starting DST Export ===");
      if (_DEBUG) console.log("Canvas size:", _stitchData.width, _stitchData.height);
      if (_DEBUG) console.log("Stitch data:", _stitchData);

      let currentThreadIndex = -1;

      for (let threadIndex = 0; threadIndex < _stitchData.threads.length; threadIndex++) {
        const thread = _stitchData.threads[threadIndex];

        // Skip threads with no stitches
        if (thread.runs.length === 0 || !thread.runs.some((run) => run.length > 0)) {
          continue;
        }

        // If we're changing threads and have previous stitches, add a color change command
        if (currentThreadIndex !== -1 && threadIndex !== currentThreadIndex && points.length > 0) {
          // Get the last stitch position
          const lastPoint = points[points.length - 1];

          // Add a color change command at the same position
          points.push({
            x: lastPoint.x,
            y: lastPoint.y,
            colorChange: true,
          });

          if (_DEBUG) console.log("Color change at:", lastPoint.x, lastPoint.y);
        }

        currentThreadIndex = threadIndex;

        for (const run of thread.runs) {
          // Check if this is a thread trim command
          if (run.length === 1 && run[0].command === "trim") {
            if (_DEBUG) {
              console.log("Trim command at:", run[0].x, run[0].y);
              console.log("Canvas size:", _stitchData.width, _stitchData.height);
            }

            // Validate trim command coordinates
            if (run[0].x == null || run[0].y == null || !isFinite(run[0].x) || !isFinite(run[0].y)) {
              if (_DEBUG) console.warn("Skipping invalid trim command with null/NaN coordinates:", run[0]);
              continue;
            }

            // Convert from mm to 0.1mm for DST format
            points.push({
              x: run[0].x * 10, // Convert from mm to 0.1mm for DST format
              y: run[0].y * 10, // Convert from mm to 0.1mm for DST format
              jump: true,
              trim: true,
            });
            continue;
          }

          // Normal stitches
          if (_DEBUG) console.log("=== New Stitch Run ===");
          if (_DEBUG) console.log("Run:", run);
          for (const stitch of run) {
            // Validate stitch coordinates before processing
            if (stitch.x == null || stitch.y == null || !isFinite(stitch.x) || !isFinite(stitch.y)) {
              if (_DEBUG) console.warn("Skipping invalid stitch with null/NaN coordinates:", stitch);
              continue;
            }

            // if (_DEBUG)
            //   console.log("Stitch point:", {
            //     mm: { x: stitch.x, y: stitch.y },
            //     dst: { x: stitch.x * 10, y: stitch.y * 10 }, // Convert to DST units (0.1mm) for logging
            //   });

            // Convert from mm to 0.1mm for DST format
            points.push({
              x: stitch.x * 10, // Convert to DST units (0.1mm)
              y: stitch.y * 10, // Convert to DST units (0.1mm)
              command: stitch.command,
              jump: stitch.command === "jump",
            });
          }
        }
      }

      // Skip export if no points
      if (points.length === 0) {
        console.warn("No embroidery points to export");
        return;
      }

      if (_DEBUG) {
        console.log("=== Final Points Array ===");
        console.log("Total points:", points.length);
        console.log("First point:", points[0]);
        console.log("Last point:", points[points.length - 1]);

        // Log bounding box
        let minX = Infinity,
          maxX = -Infinity,
          minY = Infinity,
          maxY = -Infinity;
        for (const point of points) {
          minX = Math.min(minX, point.x);
          maxX = Math.max(maxX, point.x);
          minY = Math.min(minY, point.y);
          maxY = Math.max(maxY, point.y);
        }
        console.log("Bounding box (0.1mm):", {
          minX,
          maxX,
          minY,
          maxY,
          width: maxX - minX,
          height: maxY - minY,
        });
      }

      dstWriter.saveDST(points, "EmbroideryPattern", filename);
    };

    /**
     * Inserts a thread trim command at the current position.
     * @method trimThread
     * @for p5
     * @example
     *
     *
     * function setup() {
     *   createCanvas(400, 400);
     *   beginRecord(this);
     *   line(10, 10, 50, 50);
     *   trimThread(); // Cut thread at current position
     *   line(60, 60, 100, 100);
     * }
     *
     *
     */
    p5embroidery.trimThread = function (threadIndex = _strokeThreadIndex) {
      if (_recording) {
        // Get the current thread
        const currentThread = _stitchData.threads[threadIndex];

        // // Check if there are any runs in the current thread
        // if (!currentThread || currentThread.runs.length === 0) {
        //   console.warn("trimThread: No runs found for thread", threadIndex);

        //   return; // Nothing to trim
        // }

        // Get the last run in the current thread
        const lastRun = currentThread.runs[currentThread.runs.length - 1];

        // Check if the last run has any stitches
        if (!lastRun || lastRun.length === 0) {
          console.warn("trimThread: No stitches to trim for thread", threadIndex);
          return; // No stitches to trim
        }

        // Get the last stitch position from the last run (in mm)
        let lastStitchIndex = lastRun.length - 1;
        let currentX = lastRun[lastStitchIndex].x;
        let currentY = lastRun[lastStitchIndex].y;

        if (_DEBUG) console.log("Adding trim at position:", currentX, currentY);

        // Add a special point to indicate thread trim (in mm)
        _stitchData.threads[threadIndex].runs.push([
          {
            x: currentX,
            y: currentY,
            command: "trim",
          },
        ]);

        if (_drawMode === "stitch") {
          // draw a scissors emoji at the trim point
          _p5Instance.push();
          _originalFillFunc.call(_p5Instance, 0);
          let lineLength = 10;
          let endX = mmToPixel(currentX) + lineLength;
          let endY = mmToPixel(currentY) - lineLength;
          _originalStrokeFunc.call(_p5Instance, 255, 0, 0); // red for line
          _originalStrokeWeightFunc.call(_p5Instance, 0.5);

          _originalLineFunc.call(_p5Instance, mmToPixel(currentX), mmToPixel(currentY), endX, endY);
          // Place translucent white circle at the center of the scissors
          _p5Instance.push();
          _originalNoStrokeFunc.call(_p5Instance);
          _originalFillFunc.call(_p5Instance, 255, 255, 255, 150);
          _p5Instance.ellipseMode(CENTER);
          _originalEllipseFunc.call(_p5Instance, endX + 6, endY - 5, 20, 20);
          _p5Instance.pop();
          // Place scissors at end of line
          _p5Instance.text("✂️", endX, endY);
          _p5Instance.pop();
        }
      }
    };

    /**
     * Draws stitches according to the current draw mode.
     * @method drawStitches
     * @private
     * @param {Array} stitches - Array of stitch objects with x and y coordinates in mm
     * @param {number} threadIndex - Index of the current thread
     */
    function drawStitches(stitches, threadIndex) {
      // Check for empty stitches array
      if (!stitches || stitches.length === 0) {
        console.warn("drawStitches: Empty stitches array");
        return;
      }

      let prevX = mmToPixel(stitches[0].x);
      let prevY = mmToPixel(stitches[0].y);

      if (_drawMode === "stitch") {
        // Draw stitch lines
        _p5Instance.push();

        for (let i = 1; i < stitches.length; i++) {
          let currentX = mmToPixel(stitches[i].x);
          let currentY = mmToPixel(stitches[i].y);

          if (i === 1) {
            // Draw small dots at stitch points
            _originalStrokeFunc.call(_p5Instance, 255, 0, 0); // Red for stitch points
            _originalStrokeWeightFunc.call(_p5Instance, 3);
            _originalPointFunc.call(_p5Instance, prevX, prevY);
          }

          // Use the current thread color if defined, otherwise black
          _originalStrokeFunc.call(
            _p5Instance,
            _stitchData.threads[threadIndex].color.r,
            _stitchData.threads[threadIndex].color.g,
            _stitchData.threads[threadIndex].color.b,
          );
          _originalStrokeWeightFunc.call(_p5Instance, 1);
          _originalLineFunc.call(_p5Instance, prevX, prevY, currentX, currentY);

          // Draw small dots at stitch points
          _originalStrokeFunc.call(_p5Instance, 255, 0, 0); // Red for stitch points
          _originalStrokeWeightFunc.call(_p5Instance, 3);
          _originalPointFunc.call(_p5Instance, currentX, currentY);

          prevX = currentX;
          prevY = currentY;
        }
        _p5Instance.pop();
      } else if (_drawMode === "realistic") {
        _p5Instance.push();
        _p5Instance.strokeCap(ROUND);

        // Draw background dots for thread ends

        for (let i = 1; i < stitches.length; i++) {
          let currentX = mmToPixel(stitches[i].x);
          let currentY = mmToPixel(stitches[i].y);
          _originalNoStrokeFunc.call(_p5Instance);
          _originalFillFunc.call(_p5Instance, 15); // White background dots

          _originalEllipseFunc.call(_p5Instance, currentX, currentY, 3); // Small white dots at stitch points

          // Draw three layers of lines with different weights and colors
          // Dark bottom layer - darkened thread color
          _originalStrokeFunc.call(
            _p5Instance,
            _stitchData.threads[threadIndex].color.r * 0.4,
            _stitchData.threads[threadIndex].color.g * 0.4,
            _stitchData.threads[threadIndex].color.b * 0.4,
          );
          _originalStrokeWeightFunc.call(_p5Instance, 2.5);
          _originalLineFunc.call(_p5Instance, prevX, prevY, currentX, currentY);

          // Middle layer - thread color
          _originalStrokeFunc.call(
            _p5Instance,
            _stitchData.threads[threadIndex].color.r,
            _stitchData.threads[threadIndex].color.g,
            _stitchData.threads[threadIndex].color.b,
          );
          _originalStrokeWeightFunc.call(_p5Instance, 1.8);
          _originalLineFunc.call(_p5Instance, prevX, prevY, currentX, currentY);

          // Top highlight layer - lightened thread color
          _originalStrokeFunc.call(
            _p5Instance,
            _stitchData.threads[threadIndex].color.r * 1.8,
            _stitchData.threads[threadIndex].color.g * 1.8,
            _stitchData.threads[threadIndex].color.b * 1.8,
          );
          _originalStrokeWeightFunc.call(_p5Instance, 1);
          _originalLineFunc.call(_p5Instance, prevX, prevY, currentX, currentY);
          prevX = currentX;
          prevY = currentY;
        }
        _p5Instance.strokeCap(SQUARE);
        _p5Instance.pop();
      }

      // Return the last stitch position for chaining
      return stitches.length > 0
        ? {
            x: stitches[stitches.length - 1].x,
            y: stitches[stitches.length - 1].y,
          }
        : { x: startX, y: startY };
    }

    function getPathBounds(points) {
      const xs = points.map((p) => p.x);
      const ys = points.map((p) => p.y);
      const minX = Math.min(...xs);
      const maxX = Math.max(...xs);
      const minY = Math.min(...ys);
      const maxY = Math.max(...ys);

      return {
        x: minX,
        y: minY,
        w: maxX - minX,
        h: maxY - minY,
      };
    }

    function createTatamiFillFromPath(pathPoints, settings) {
      // Default settings
      const angle = settings.angle || 0;
      const spacing = settings.rowSpacing || 0.8;
      settings.stitchLength || 3;

      // Calculate bounds of the polygon
      const bounds = getPathBounds(pathPoints);

      // Calculate the center of the path
      const centerX = bounds.x + bounds.w / 2;
      const centerY = bounds.y + bounds.h / 2;

      // Expand bounds to ensure we cover rotated shape
      const diagonal = Math.sqrt(bounds.w * bounds.w + bounds.h * bounds.h) * 1.2;

      // First pass: collect all valid segments organized by scan line
      const scanLineSegments = [];
      let forward = true;

      // Generate scan lines at the specified angle
      for (let d = -diagonal / 2; d <= diagonal / 2; d += spacing) {
        // Calculate start and end points for the scan line
        const startX = centerX - (diagonal / 2) * Math.cos(angle) - d * Math.sin(angle);
        const startY = centerY - (diagonal / 2) * Math.sin(angle) + d * Math.cos(angle);
        const endX = centerX + (diagonal / 2) * Math.cos(angle) - d * Math.sin(angle);
        const endY = centerY + (diagonal / 2) * Math.sin(angle) + d * Math.cos(angle);

        // Find intersections with the polygon
        const intersections = segmentIntersectPolygon({ x: startX, y: startY }, { x: endX, y: endY }, pathPoints);

        // Sort intersections by distance from start
        intersections.sort((a, b) => {
          const distA = Math.sqrt((a.x - startX) * (a.x - startX) + (a.y - startY) * (a.y - startY));
          const distB = Math.sqrt((b.x - startX) * (b.x - startX) + (b.y - startY) * (b.y - startY));
          return distA - distB;
        });

        // Find valid segments for this scan line
        const validSegments = findValidSegments(
          intersections,
          pathPoints);

        // Store segments with their scan line info
        for (const segment of validSegments) {
          scanLineSegments.push({
            start: segment.start,
            end: segment.end,
            scanLineIndex: scanLineSegments.length,
            forward: forward,
          });
        }

        // Alternate direction for next row
        forward = !forward;
      }

      // Second pass: group segments by proximity and create optimized stitch paths
      const stitches = createOptimizedStitchPaths(scanLineSegments, settings);

      return stitches;
    }

    // Function to create optimized stitch paths by grouping nearby segments
    function createOptimizedStitchPaths(segments, settings = {}) {
      if (segments.length === 0) return [];

      const stitches = [];
      const used = new Array(segments.length).fill(false);
      const jumpThreshold = 10; // mm - threshold for inserting trim commands

      // Process segments in groups to minimize jumps
      for (let i = 0; i < segments.length; i++) {
        if (used[i]) continue;

        // Start a new region from this segment
        const currentRegion = [];
        const stack = [i];

        // Find all segments connected to this region
        while (stack.length > 0) {
          const currentIndex = stack.pop();
          if (used[currentIndex]) continue;

          used[currentIndex] = true;
          currentRegion.push(segments[currentIndex]);

          // Find nearby segments (within reasonable distance)
          for (let j = 0; j < segments.length; j++) {
            if (used[j]) continue;

            const currentSeg = segments[currentIndex];
            const testSeg = segments[j];

            // Check if segments are close enough to be in same region
            const minDist = Math.min(
              Math.sqrt(
                (currentSeg.start.x - testSeg.start.x) * (currentSeg.start.x - testSeg.start.x) +
                  (currentSeg.start.y - testSeg.start.y) * (currentSeg.start.y - testSeg.start.y),
              ),
              Math.sqrt(
                (currentSeg.start.x - testSeg.end.x) * (currentSeg.start.x - testSeg.end.x) +
                  (currentSeg.start.y - testSeg.end.y) * (currentSeg.start.y - testSeg.end.y),
              ),
              Math.sqrt(
                (currentSeg.end.x - testSeg.start.x) * (currentSeg.end.x - testSeg.start.x) +
                  (currentSeg.end.y - testSeg.start.y) * (currentSeg.end.y - testSeg.start.y),
              ),
              Math.sqrt(
                (currentSeg.end.x - testSeg.end.x) * (currentSeg.end.x - testSeg.end.x) +
                  (currentSeg.end.y - testSeg.end.y) * (currentSeg.end.y - testSeg.end.y),
              ),
            );

            // If segments are close (within 2 scan line spacings), add to region
            if (minDist < 20) {
              // Adjust this threshold as needed
              stack.push(j);
            }
          }
        }

        // Sort segments in current region for optimal stitching order
        const optimizedRegion = optimizeRegionStitchOrder(currentRegion, settings);

        // Add trim command before this region if it's not the first region and there are existing stitches
        if (stitches.length > 0 && optimizedRegion.length > 0) {
          // Get the last stitch position
          const lastStitch = stitches[stitches.length - 1];
          const firstStitch = optimizedRegion[0];

          // Calculate distance to first stitch of new region
          const jumpDistance = Math.sqrt(
            (firstStitch.x - lastStitch.x) * (firstStitch.x - lastStitch.x) +
              (firstStitch.y - lastStitch.y) * (firstStitch.y - lastStitch.y),
          );

          // Insert trim command if jump is too long
          if (jumpDistance > jumpThreshold) {
            stitches.push({
              x: lastStitch.x,
              y: lastStitch.y,
              command: "trim",
            });
          }
        }

        // Add region stitches to final array
        stitches.push(...optimizedRegion);
      }

      return stitches;
    }

    // Function to optimize stitch order within a region
    function optimizeRegionStitchOrder(regionSegments, settings = {}) {
      const stitchSettings = {
        stitchLength: settings.stitchLength || 2,
        minStitchLength: settings.minStitchLength || 0.5,
        resampleNoise: settings.resampleNoise || 0,
      };

      if (regionSegments.length === 0) return [];
      if (regionSegments.length === 1) {
        const seg = regionSegments[0];
        const segmentPath = [
          { x: seg.forward ? seg.start.x : seg.end.x, y: seg.forward ? seg.start.y : seg.end.y },
          { x: seg.forward ? seg.end.x : seg.start.x, y: seg.forward ? seg.end.y : seg.start.y },
        ];
        // Convert path to individual stitches
        return convertPathToStitches(segmentPath, stitchSettings);
      }

      const stitches = [];
      const used = new Array(regionSegments.length).fill(false);
      used[0] = true;

      let currentSeg = regionSegments[0];
      const firstSegmentPath = [
        {
          x: currentSeg.forward ? currentSeg.start.x : currentSeg.end.x,
          y: currentSeg.forward ? currentSeg.start.y : currentSeg.end.y,
        },
        {
          x: currentSeg.forward ? currentSeg.end.x : currentSeg.start.x,
          y: currentSeg.forward ? currentSeg.end.y : currentSeg.start.y,
        },
      ];
      // Convert first segment to stitches
      const firstSegmentStitches = convertPathToStitches(firstSegmentPath, stitchSettings);
      stitches.push(...firstSegmentStitches);

      // Find the nearest unused segment for each subsequent stitch
      for (let i = 1; i < regionSegments.length; i++) {
        let nearestIndex = -1;
        let nearestDist = Infinity;

        const lastStitch = stitches[stitches.length - 1];

        for (let j = 0; j < regionSegments.length; j++) {
          if (used[j]) continue;

          const testSeg = regionSegments[j];

          // Calculate distance from end of last stitch to start of test segment
          const distToStart = Math.sqrt(
            (lastStitch.x - testSeg.start.x) * (lastStitch.x - testSeg.start.x) +
              (lastStitch.y - testSeg.start.y) * (lastStitch.y - testSeg.start.y),
          );
          const distToEnd = Math.sqrt(
            (lastStitch.x - testSeg.end.x) * (lastStitch.x - testSeg.end.x) +
              (lastStitch.y - testSeg.end.y) * (lastStitch.y - testSeg.end.y),
          );

          const minDist = Math.min(distToStart, distToEnd);

          if (minDist < nearestDist) {
            nearestDist = minDist;
            nearestIndex = j;
          }
        }

        if (nearestIndex !== -1) {
          used[nearestIndex] = true;
          const nextSeg = regionSegments[nearestIndex];

          // // Check if we need a trim command for a long jump within the region
          // if (nearestDist > intraRegionJumpThreshold) {
          //   stitches.push({
          //     x: lastStitch.x,
          //     y: lastStitch.y,
          //     command: "trim"
          //   });
          // }

          // Determine orientation based on which end is closer
          const distToStart = Math.sqrt(
            (lastStitch.x - nextSeg.start.x) * (lastStitch.x - nextSeg.start.x) +
              (lastStitch.y - nextSeg.start.y) * (lastStitch.y - nextSeg.start.y),
          );
          const distToEnd = Math.sqrt(
            (lastStitch.x - nextSeg.end.x) * (lastStitch.x - nextSeg.end.x) +
              (lastStitch.y - nextSeg.end.y) * (lastStitch.y - nextSeg.end.y),
          );

          const useForwardDirection = distToStart <= distToEnd;

          // Create path for this segment
          const segmentPath = [
            {
              x: useForwardDirection ? nextSeg.start.x : nextSeg.end.x,
              y: useForwardDirection ? nextSeg.start.y : nextSeg.end.y,
            },
            {
              x: useForwardDirection ? nextSeg.end.x : nextSeg.start.x,
              y: useForwardDirection ? nextSeg.end.y : nextSeg.start.y,
            },
          ];

          // Convert segment to individual stitches
          const segmentStitches = convertPathToStitches(segmentPath, stitchSettings);
          stitches.push(...segmentStitches);
        }
      }

      return stitches;
    }

    // Function to find valid segments that are inside the polygon
    function findValidSegments(intersections, polygon, lineStart, lineEnd) {
      const validSegments = [];

      // For each pair of intersections, check if the segment between them is inside the polygon
      for (let i = 0; i < intersections.length - 1; i += 2) {
        if (i + 1 >= intersections.length) break;

        const segStart = intersections[i];
        const segEnd = intersections[i + 1];

        // Check if the midpoint of this segment is inside the polygon
        const midX = (segStart.x + segEnd.x) / 2;
        const midY = (segStart.y + segEnd.y) / 2;

        if (pointInPolygon({ x: midX, y: midY }, polygon)) {
          validSegments.push({
            start: segStart,
            end: segEnd,
          });
        }
      }

      return validSegments;
    }

    // Point-in-polygon test using ray casting algorithm
    function pointInPolygon(point, polygon) {
      let inside = false;

      for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
        const xi = polygon[i].x;
        const yi = polygon[i].y;
        const xj = polygon[j].x;
        const yj = polygon[j].y;

        if (yi > point.y !== yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi) {
          inside = !inside;
        }
      }

      return inside;
    }

    // Function to find intersections between a line segment and a polygon
    function segmentIntersectPolygon(p1, p2, polygon) {
      const intersections = [];

      // Check each edge of the polygon
      for (let i = 0; i < polygon.length - 1; i++) {
        const p3 = polygon[i];
        const p4 = polygon[i + 1];

        // Check if the line segments intersect
        const intersection = lineLineIntersection(p1, p2, p3, p4);
        if (intersection) {
          intersections.push(intersection);
        }
      }

      // Check the last edge (connecting the last point to the first)
      if (polygon.length > 0) {
        const p3 = polygon[polygon.length - 1];
        const p4 = polygon[0];
        const intersection = lineLineIntersection(p1, p2, p3, p4);
        if (intersection) {
          intersections.push(intersection);
        }
      }

      return intersections;
    }

    // Function to calculate the intersection point of two line segments
    function lineLineIntersection(p1, p2, p3, p4) {
      // Line segment 1: p1 to p2
      // Line segment 2: p3 to p4

      const denominator = (p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y);

      // Lines are parallel or coincident
      if (denominator === 0) {
        return null;
      }

      const ua = ((p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x)) / denominator;
      const ub = ((p2.x - p1.x) * (p1.y - p3.y) - (p2.y - p1.y) * (p1.x - p3.x)) / denominator;

      // Check if intersection is within both line segments
      if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
        const intersectionX = p1.x + ua * (p2.x - p1.x);
        const intersectionY = p1.y + ua * (p2.y - p1.y);
        return { x: intersectionX, y: intersectionY };
      }

      // No intersection within the line segments
      return null;
    }

    /**
     * Creates satin fill stitches from a path.
     * @private
     * @param {Array<{x: number, y: number}>} pathPoints - Array of path points in mm
     * @param {Object} settings - Fill settings
     * @returns {Array<{x: number, y: number}>} Array of stitch points in mm
     */
    function createSatinFillFromPath(pathPoints, settings) {
      // For now, fall back to tatami fill
      // TODO: Implement proper satin fill algorithm
      return createTatamiFillFromPath(pathPoints, settings);
    }

    /**
     * Creates spiral fill stitches from a path.
     * @private
     * @param {Array<{x: number, y: number}>} pathPoints - Array of path points in mm
     * @param {Object} settings - Fill settings
     * @returns {Array<{x: number, y: number}>} Array of stitch points in mm
     */
    function createSpiralFillFromPath(pathPoints, settings) {
      if (!pathPoints || pathPoints.length < 3) {
        if (_DEBUG) console.log("createSpiralFillFromPath: insufficient pathPoints", pathPoints?.length);
        return [];
      }

      const stitches = [];
      const bounds = getPathBounds(pathPoints);
      const centerX = bounds.x + bounds.w / 2;
      const centerY = bounds.y + bounds.h / 2;

      // Calculate maximum radius to ensure we fill the entire shape
      const maxRadius = (Math.max(bounds.w, bounds.h) / 2) * 1.2;

      // Spiral parameters
      const spiralSpacing = settings.rowSpacing || 0.8;
      const stitchLength = settings.stitchLength || 2;

      let currentRadius = 0;
      let currentAngle = 0;
      let lastPoint = null;

      if (_DEBUG) {
        console.log("Spiral fill params:", { bounds, centerX, centerY, maxRadius, spiralSpacing, stitchLength });
      }

      // Generate spiral points from center outward
      while (currentRadius <= maxRadius) {
        const x = centerX + Math.cos(currentAngle) * currentRadius;
        const y = centerY + Math.sin(currentAngle) * currentRadius;

        // Check if point is inside the polygon
        if (pointInPolygon({ x, y }, pathPoints)) {
          if (lastPoint) {
            // Add stitches along the path from last point to current point
            const segmentStitches = straightLineStitch(lastPoint.x, lastPoint.y, x, y, {
              stitchLength: stitchLength,
              minStitchLength: settings.minStitchLength || 0.5,
              resampleNoise: settings.resampleNoise || 0,
            });
            stitches.push(...segmentStitches);
          } else {
            // First point
            stitches.push({ x, y });
          }
          lastPoint = { x, y };
        }

        // Update angle and radius for next iteration
        const angleIncrement = stitchLength / Math.max(currentRadius, 1); // Prevent division by zero
        currentAngle += angleIncrement;
        currentRadius += (spiralSpacing * angleIncrement) / (2 * Math.PI);

        // Safety break to prevent infinite loops
        if (currentAngle > 100 * Math.PI) {
          if (_DEBUG) console.log("Spiral fill: breaking due to too many iterations");
          break;
        }
      }

      if (_DEBUG) {
        console.log("Spiral fill generated stitches:", stitches.length);
      }

      return stitches;
    }

    /**
     * Creates tatami fill with contours (holes).
     * @private
     * @param {Array<{x: number, y: number}>} mainPath - Main outline path
     * @param {Array<Array<{x: number, y: number}>>} contours - Array of contour paths (holes)
     * @param {Object} settings - Fill settings
     * @returns {Array<{x: number, y: number}>} Array of stitch points in mm
     */
    function createTatamiFillWithContours(mainPath, contours, settings) {
      if (!mainPath || mainPath.length < 3) {
        if (_DEBUG) console.log("createTatamiFillWithContours: insufficient mainPath points");
        return [];
      }

      // Default settings
      const angle = settings.angle || 0;
      const spacing = settings.rowSpacing || 0.8;
      settings.stitchLength || 3;

      // Calculate bounds of the main polygon
      const bounds = getPathBounds(mainPath);

      // Calculate the center of the path
      const centerX = bounds.x + bounds.w / 2;
      const centerY = bounds.y + bounds.h / 2;

      // Expand bounds to ensure we cover rotated shape
      const diagonal = Math.sqrt(bounds.w * bounds.w + bounds.h * bounds.h) * 1.2;

      // First pass: collect all valid segments organized by scan line
      const scanLineSegments = [];
      let forward = true;

      // Generate scan lines at the specified angle
      for (let d = -diagonal / 2; d <= diagonal / 2; d += spacing) {
        // Calculate start and end points for the scan line
        const startX = centerX - (diagonal / 2) * Math.cos(angle) - d * Math.sin(angle);
        const startY = centerY - (diagonal / 2) * Math.sin(angle) + d * Math.cos(angle);
        const endX = centerX + (diagonal / 2) * Math.cos(angle) - d * Math.sin(angle);
        const endY = centerY + (diagonal / 2) * Math.sin(angle) + d * Math.cos(angle);

        const scanLine = { x: startX, y: startY };
        const scanLineEnd = { x: endX, y: endY };

        // Find intersections with the main polygon
        const mainIntersections = segmentIntersectPolygon(scanLine, scanLineEnd, mainPath);

        // Find intersections with all contour polygons
        let allContourIntersections = [];
        for (const contour of contours) {
          const contourIntersections = segmentIntersectPolygon(scanLine, scanLineEnd, contour);
          allContourIntersections = allContourIntersections.concat(contourIntersections);
        }

        // Combine and sort all intersections by distance from start
        const allIntersections = mainIntersections.concat(allContourIntersections);
        allIntersections.sort((a, b) => {
          const distA = Math.sqrt((a.x - startX) * (a.x - startX) + (a.y - startY) * (a.y - startY));
          const distB = Math.sqrt((b.x - startX) * (b.x - startX) + (b.y - startY) * (b.y - startY));
          return distA - distB;
        });

        // Find valid segments considering contours
        const validSegments = findValidSegmentsWithContours(allIntersections, mainPath, contours);

        // Store segments with their scan line info
        for (const segment of validSegments) {
          scanLineSegments.push({
            start: segment.start,
            end: segment.end,
            scanLineIndex: scanLineSegments.length,
            forward: forward,
          });
        }

        // Alternate direction for next row
        forward = !forward;
      }

      // Second pass: group segments by proximity and create optimized stitch paths
      const stitches = createOptimizedStitchPaths(scanLineSegments, settings);

      return stitches;
    }

    /**
     * Find valid segments that are inside the main polygon but outside any contours.
     * @private
     */
    function findValidSegmentsWithContours(intersections, mainPolygon, contours, lineStart, lineEnd) {
      const validSegments = [];

      // For each pair of intersections, check if the segment between them is valid
      for (let i = 0; i < intersections.length - 1; i += 2) {
        if (i + 1 >= intersections.length) break;

        const segStart = intersections[i];
        const segEnd = intersections[i + 1];

        // Check if the midpoint of this segment is inside the main polygon
        const midX = (segStart.x + segEnd.x) / 2;
        const midY = (segStart.y + segEnd.y) / 2;
        const midpoint = { x: midX, y: midY };

        // Must be inside main polygon
        if (!pointInPolygon(midpoint, mainPolygon)) {
          continue;
        }

        // Must not be inside any contour
        let insideContour = false;
        for (const contour of contours) {
          if (pointInPolygon(midpoint, contour)) {
            insideContour = true;
            break;
          }
        }

        if (!insideContour) {
          validSegments.push({
            start: segStart,
            end: segEnd,
          });
        }
      }

      return validSegments;
    }

    // Expose public functions
    global.p5embroidery = p5embroidery;
    global.beginRecord = p5embroidery.beginRecord;
    global.endRecord = p5embroidery.endRecord;
    global.exportEmbroidery = p5embroidery.exportEmbroidery;
    global.exportDST = p5embroidery.exportDST;
    global.exportGcode = p5embroidery.exportGcode;
    global.trimThread = p5embroidery.trimThread; // Renamed from cutThread
    global.setStitch = p5embroidery.setStitch;
    global.setDrawMode = p5embroidery.setDrawMode;
    global.drawStitches = p5embroidery.drawStitches;
    global.mmToPixel = mmToPixel;
    global.pixelToMm = pixelToMm;
    global.setStrokeMode = p5embroidery.setStrokeMode;
    global.setStrokeJoin = p5embroidery.setStrokeJoin;
    global.STROKE_MODE = STROKE_MODE;
    global.STROKE_JOIN = STROKE_JOIN;
    global.FILL_MODE = FILL_MODE;
    global.setFillMode = p5embroidery.setFillMode;
    global.setFillSettings = p5embroidery.setFillSettings;
    global.setStrokeSettings = p5embroidery.setStrokeSettings;

    // Expose new path-based functions
    global.convertPathToStitches = convertPathToStitches;
    global.multiLineStitchingFromPath = multiLineStitchFromPath;
    global.sashikoStitchingFromPath = sashikoStitchFromPath;
    global.zigzagStitchFromPath = zigzagStitchFromPath;

    // Expose debug function
    global.setDebugMode = setDebugMode;

    // Expose contour functions
    global.beginContour =
      p5embroidery.beginContour ||
      function () {
        if (window.beginContour && typeof window.beginContour === "function") {
          return window.beginContour.apply(this, arguments);
        }
      };
    global.endContour =
      p5embroidery.endContour ||
      function () {
        if (window.endContour && typeof window.endContour === "function") {
          return window.endContour.apply(this, arguments);
        }
      };
  })(typeof globalThis !== "undefined" ? globalThis : window);

  /**
   * Converts millimeters to pixels.
   * @method mmToPixel
   * @for p5
   * @param {Number} mm - Millimeters
   * @param {Number} [dpi=96] - Dots per inch
   * @return {Number} Pixels
   * @example
   *
   *
   * function setup() {
   *   let pixels = mmToPixel(10); // Convert 10mm to pixels
   *   if(_DEBUG) console.log(pixels);
   * }
   *
   *
   */
  function mmToPixel(mm, dpi = 96) {
    return (mm / 25.4) * dpi;
  }

  /**
   * Converts pixels to millimeters.
   * @method pixelToMm
   * @for p5
   * @param {Number} pixels - Pixels
   * @param {Number} [dpi=96] - Dots per inch
   * @return {Number} Millimeters
   * @example
   *
   *
   * function setup() {
   *   let mm = pixelToMm(100); // Convert 100 pixels to mm
   *   if(_DEBUG) console.log(mm);
   * }
   *
   *
   */
  function pixelToMm(pixels, dpi = 96) {
    return (pixels * 25.4) / dpi;
  }

  /**
   * Creates straight line stitches from an array of path points
   * @private
   * @param {Array<{x: number, y: number}>} pathPoints - Array of path points in mm
   * @param {Object} stitchSettings - Settings for the stitches
   * @returns {Array<{x: number, y: number}>} Array of stitch points in mm
   */
  function straightLineStitchFromPath(pathPoints, stitchSettings = _embroiderySettings) {
    if (!pathPoints || pathPoints.length < 2) {
      console.warn("Cannot create straight stitching from insufficient path points");
      return [];
    }

    const result = [];

    // Process each segment between consecutive points
    for (let i = 0; i < pathPoints.length - 1; i++) {
      const p1 = pathPoints[i];
      const p2 = pathPoints[i + 1];

      // For the first segment, include the starting point
      if (i === 0) {
        result.push({
          x: p1.x,
          y: p1.y,
        });
      }

      // Calculate segment properties
      const dx = p2.x - p1.x;
      const dy = p2.y - p1.y;
      const distance = Math.sqrt(dx * dx + dy * dy);

      // Skip if the segment is too short
      if (distance < stitchSettings.minStitchLength) {
        // Still include the endpoint
        result.push({
          x: p2.x,
          y: p2.y,
        });
        continue;
      }

      // Calculate number of stitches for this segment
      let baseStitchLength = stitchSettings.stitchLength;
      let numStitches = Math.floor(distance / baseStitchLength);
      let currentDistance = 0;

      // Create intermediate stitches along this segment
      for (let j = 0; j < numStitches; j++) {
        // Add noise to stitch length if specified
        let stitchLength = baseStitchLength;
        if (stitchSettings.resampleNoise > 0) {
          let noise = (Math.random() * 2 - 1) * stitchSettings.resampleNoise;
          stitchLength *= 1 + noise;
        }

        // Update cumulative distance
        currentDistance += stitchLength;
        let t = Math.min(currentDistance / distance, 1);

        // Add the stitch point
        result.push({
          x: p1.x + dx * t,
          y: p1.y + dy * t,
        });
      }

      // Add endpoint of this segment
      let remainingDistance = distance - currentDistance;
      if (remainingDistance > stitchSettings.minStitchLength || numStitches === 0) {
        result.push({
          x: p2.x,
          y: p2.y,
        });
      }
    }

    if (_DEBUG) console.log("Generated straight line path stitches:", result);
    return result;
  }

}));