p5.embroider
Version:
a p5.js library for creating digital embroidery patterns
1,591 lines (1,424 loc) • 99.7 kB
JavaScript
(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