UNPKG

p5.embroider

Version:

a p5.js library for creating digital embroidery patterns

1,591 lines (1,424 loc) 99.7 kB
(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: [], pixelsPerUnit: 1, stitchCount: 0, }; // Vertex properties let _shapeKind = null; 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) { if ( kind === window.POINTS || kind === window.LINES || kind === window.TRIANGLES || kind === window.TRIANGLE_FAN || kind === window.TRIANGLE_STRIP || kind === window.QUADS || kind === window.QUAD_STRIP ) { _shapeKind = kind; } else { _shapeKind = null; } _vertices = []; 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; } { console.log("endShape", _vertices, _vertices.length); console.log("_doStroke", _doStroke); console.log("_doFill", _doFill); } if (!_doStroke && !_doFill) { console.log("🪡 p5.embroider says: _doStroke and _doFill are both false"); return this; } const closeShape = mode === window.CLOSE; if (closeShape && !_isContour) { _vertices.push(_vertices[0]); } if(_doFill) { // Convert vertices to pathPoints format for the fill function const 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); // Debug log { console.log("endShape: Converted vertices to stitches:", { vertices: _vertices.length, stitches: stitches.length, shapeKind: _shapeKind, mode: _drawMode, }); } //add stitches to the embroidery data _stitchData.threads[_strokeThreadIndex].runs.push(stitches); if (_drawMode === "stitch" || _drawMode === "realistic") { console.log("Drawing stitches:", { count: stitches.length, threadIndex: _strokeThreadIndex, mode: _drawMode, firstStitch: stitches.length > 0 ? stitches[0] : null, lastStitch: stitches.length > 0 ? stitches[stitches.length - 1] : null, }); drawStitches(stitches, _strokeThreadIndex); } else if (_drawMode === "p5") { _originalEndShapeFunc.call(_p5Instance, mode, count); } _isContour = false; // 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); console.log("_vertices", _vertices); } 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) { console.log("Converting line to stitches (before offset):", { from: { x: x1, y: y1 }, to: { x: x2, y: y2 }, }); console.log("Converting line to stitches", { from: { x: x1, y: y1 }, to: { x: x2, y: y2 }, }); let dx = x2 - x1; let dy = y2 - y1; let distance = Math.sqrt(dx * dx + dy * dy); console.log("Line properties:", { dx, dy, distance, minStitchLength: stitchSettings.minStitchLength, stitchLength: stitchSettings.stitchLength, strokeWeight: stitchSettings.strokeWeight, }); if (stitchSettings.strokeWeight > 0) { switch (_currentStrokeMode) { case STROKE_MODE.STRAIGHT: return straightLineStitch(x1, y1, x2, y2, stitchSettings); case STROKE_MODE.ZIGZAG: return zigzagStitch(x1, y1, x2, y2, stitchSettings); case STROKE_MODE.LINES: return multiLineStitch(x1, y1, x2, y2, stitchSettings); case STROKE_MODE.SASHIKO: return sashikoStitch(x1, y1, x2, y2, stitchSettings); default: return straightLineStitch(x1, y1, x2, y2, stitchSettings); } } else { return straightLineStitch(x1, y1, x2, y2, stitchSettings); } } /** * Converts a path into a series of stitches. * @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 */ fun