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);
    }
  }

  (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: []};
    let _vertices = [];
    let _isContour = false;

    let _strokeThreadIndex = 0;
    let _fillThreadIndex = 0;

    // 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",
    };

    let _doStroke = false; // Track if stroke is enabled
    let _currentStrokeMode = STROKE_MODE.STRAIGHT;

    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
      spacing: 3, // 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,
    };

    /**
     * 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 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.spacing] - 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.spacing !== undefined) {
        _fillSettings.spacing = settings.spacing;
      }
      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) {

          _vertices = [];

          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 (!_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 pathPoints = _vertices.map((v) => ({
              x: v.x,
              y: v.y,
            }));
            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);
            }
          }

          

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

          //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;

          // 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) {
          // Create a vertex object with named properties instead of an array
          const vert = {
            x: x,
            y: y,
            u: u || 0,
            v: v || 0,
            isVert: true,
          };

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

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

          _vertices.push(vert);
        } else {
          let args = [mmToPixel(x), mmToPixel(y), moveTo, u, v];
          _originalVertexFunc.apply(this, args);
        }
      };
    }

    /**
     * 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) {
        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 we have a stroke weight, use the appropriate path-based function
      if (strokeSettings.strokeWeight > 0) {
        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 {
        // 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) {
          let stitches = convertLineToStitches(x1, y1, x2, y2, _strokeSettings);
          _stitchData.threads[_strokeThreadIndex].runs.push(stitches);

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

    /**
     * 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
          _doFill = true;

          _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;
        }
        _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 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) {
            // 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") {
              console.log("_strokeSettings.strokeWeight", _strokeSettings.strokeWeight);
              _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 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) {
        let stitches = [];
        let fillStitches = [];
        let strokeStitches = [];

        if (_recording) {
          if (_doFill) {
            switch (_currentFillMode) {
              case FILL_MODE.TATAMI:
                fillStitches = createTatamiFill(x, y, w, h, _fillSettings);
                break;
              // Add other fill modes here
              default:
                fillStitches = createTatamiFill(x, y, w, h, _fillSettings);
            }

            stitches.push(...fillStitches);
            // Add the stitches to the current thread
            _stitchData.threads[_fillThreadIndex].runs.push(fillStitches);
          }

          if (_doStroke) {

            strokeStitches.push(...convertLineToStitches(x, y, x + w, y, _strokeSettings));
            strokeStitches.push(...convertLineToStitches(x + w, y, x + w, y + h, _strokeSettings));
            strokeStitches.push(...convertLineToStitches(x + w, y + h, x, y + h, _strokeSettings));
            strokeStitches.push(...convertLineToStitches(x, y + h, x, y, _strokeSettings));

            //stitches.push(...strokeStitches);
            _stitchData.threads[_strokeThreadIndex].runs.push(strokeStitches);
          }

          // Draw the stitches
          if (_drawMode === "stitch" || _drawMode === "realistic") {
            ///drawStitches(stitches,_strokeThreadIndex);
            //console.log("fillThreadIndex", _fillThreadIndex);
            //console.log("strokeThreadIndex", _strokeThreadIndex);

            drawStitches(fillStitches, _fillThreadIndex);
            drawStitches(strokeStitches, _strokeThreadIndex);
          } else {
            _originalStrokeWeightFunc.call(this, mmToPixel(_strokeSettings.strokeWeight));
            _originalRectFunc.call(this, mmToPixel(x), mmToPixel(y), mmToPixel(w), mmToPixel(h));
          }
        } else {
          _originalRectFunc.apply(this, arguments);
        }
      };
    }

    /**
     * Overrides necessary p5.js functions for embroidery recording.
     * @private
     */
    function overrideP5Functions() {
      overrideLineFunction();
      overrideEllipseFunction();
      overrideStrokeWeightFunction();
      overridePointFunction();
      overrideStrokeFunction();
      overrideNoStrokeFunction();
      overrideFillFunction();
      overrideNoFillFunction();
      overrideRectFunction();
      overrideVertexFunction();
      overrideBeginShapeFunction();
      overrideEndShapeFunction();
      // Add more overrides as needed
    }

    /**
     * Restores original p5.js functions.
     * @private
     */
    function restoreP5Functions() {
      window.line = _originalLineFunc;
      window.ellipse = _originalEllipseFunc;
      window.strokeWeight = _originalStrokeWeightFunc;
      window.point = _originalPointFunc;
      window.stroke = _originalStrokeFunc;
      window.noStroke = _originalNoStrokeFunc;
      window.fill = _originalFillFunc;
      window.noFill = _originalNoFillFunc;
      window.rect = _originalRectFunc;
      window.vertex = _originalVertexFunc;
      window.beginShape = _originalBeginShapeFunc;
      window.endShape = _originalEndShapeFunc;
      // 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 (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.
     * @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);

      // 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,
        });
      }
      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,
        });
      }
      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);
              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);
              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();

      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,
          });
        }

        currentThreadIndex = threadIndex;

        for (const run of thread.runs) {
          // Check if this is a thread trim command
          if (run.length === 1 && run[0].command === "trim") {

            // 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;
          }
          for (const stitch of run) {

            // 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;
      }

      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) {
          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) {
          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;

        // 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) {
      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 };
    }

    /**
     * Creates a tatami fill pattern for a rectangular area.
     * @method createTatamiFill
     * @private
     * @param {number} x - X coordinate of the rectangle in mm
     * @param {number} y - Y coordinate of the rectangle in mm
     * @param {number} w - Width of the rectangle in mm
     * @param {number} h - Height of the rectangle in mm
     * @param {Object} [stitchSettings=_fillSettings] - Fill settings object
     * @returns {Array<{x: number, y: number}>} Array of stitch points in mm
     */
    function createTatamiFill(x, y, w, h, stitchSettings = _fillSettings) {
      const stitches = [];
      const angle = stitchSettings.angle;
      stitchSettings.stitchLength;
      const stitchWidth = stitchSettings.stitchWidth;
      stitchSettings.minStitchLength;
      stitchSettings.resampleNoise;
      stitchSettings.tieDistance;

      // Convert rectangle to rotated coordinates
      const cos_angle = Math.cos(angle);
      const sin_angle = Math.sin(angle);

      // Calculate rotated bounds
      const points = [
        { x: x, y: y },
        { x: x + w, y: y },
        { x: x + w, y: y + h },
        { x: x, y: y + h },
      ];

      // Rotate points
      const rotated = points.map((p) => ({
        x: (p.x - x) * cos_angle - (p.y - y) * sin_angle,
        y: (p.x - x) * sin_angle + (p.y - y) * cos_angle,
      }));

      // Find bounds of rotated rectangle
      const minX = Math.min(...rotated.map((p) => p.x));
      const maxX = Math.max(...rotated.map((p) => p.x));
      const minY = Math.min(...rotated.map((p) => p.y));
      const maxY = Math.max(...rotated.map((p) => p.y));

      // Calculate number of rows needed
      const numRows = Math.ceil((maxY - minY) / stitchWidth);

      // Generate rows of stitches
      let forward = true;
      for (let i = 0; i <= numRows; i++) {
        const rowY = minY + i * stitchWidth;

        // Calculate row endpoints
        const rowX1 = forward ? minX : maxX;
        const rowX2 = forward ? maxX : minX;

        if (i % 2 === 0) {
          stitches.push(...straightLineStitch(rowX1, rowY, rowX2, rowY, stitchSettings));
        } else {
          stitches.push(...straightLineStitch(rowX2, rowY, rowX1, rowY, stitchSettings));
        }

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

      return stitches;
    }
    
    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.spacing || 4;
      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;
    }

    // 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.STROKE_MODE = STROKE_MODE;
    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;

  })(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,
        });
      }
    }
    return result;
  }

}));