knitout
Version:
Handy writer of knitout in javascript
539 lines (450 loc) • 17.9 kB
JavaScript
let machine = null; //can set this as machine header value (if provided), and use it to warn about unsupported extensions
// basic writer
var Writer = function(opts){
//public data:
this.carriers = {}; //names of all currently active carriers
this.needles = {}; //names of all currently-holding-loops needles
this.racking = 0; //current racking value
//private data:
this._carriers = []; //array of carrier names, front-to-back order
this._operations = []; //array of operations, stored as strings
this._headers = []; //array of headers. stored as strings
//fill '_carriers' array from opts.carriers:
if (typeof(opts) === 'undefined' || !('carriers' in opts)) {
console.warn("WARNING: options object passed to knitout.Writer does not contain a 'carriers' member. Will assume a default carrier layout (a single carrier named \"A\")");
this._carriers = ["A"];
} else {
if (!Array.isArray(opts.carriers)) throw new Error("opts.carriers should be an array of carrier names");
opts.carriers.forEach((name) => {
if (!(typeof(name) === 'string' && name.indexOf(' ') === -1)) {
throw new Error("Carrier names must be strings that do not contain the space character (' ').");
}
});
this._carriers = opts.carriers.slice();
}
//build a 'carriers' header from the '_carriers' list:
this._headers.push(";;Carriers: " + this._carriers.join(" "));
};
// function that queues header information to header list
Writer.prototype.addHeader = function (name, value) {
if (name === undefined || value === undefined) {
throw new Error("Writer.addHeader should be called with a name and a value");
}
if (!(typeof(name) === 'string' && name.indexOf(': ') === -1)) {
throw new Error("Header names must be strings that don't contain the sequence ': '");
}
if (!(typeof(value) === 'string' && value.indexOf('\n') === -1)) {
throw new Error("Header values must be strings that do not contain the LF character ('\\n').");
}
//Check for valid headers:
if (name === "Carriers") {
throw new Error("Writer.addHeader can't set Carriers header (use the 'carriers' option when creating the writer instead).");
} else if (name === "Machine") {
machine = value;
//no restrictions on value
} else if (name === "Gauge") {
if ((typeof(value) !== 'string' && !/^[0-9 ]+$/.test(value))) throw new Error(`Value of 'Gauge' header must be a string representing a number. Provided value: '${value}' is not valid.`);
} else if (name === "Position") {
let supported_positions = ['Left', 'Center', 'Right', 'Keep'];
if (!supported_positions.includes(value)) throw new Error(`'Position' header must have one of the following values: ${supported_positions.join(', ')}. Provided value: '${value}' is not valid.`);
} else if (name.startsWith("Yarn-")) {
//check for valid carrier name, warn otherwise
let carrierName = name.substr(5);
if (this._carriers.indexOf(carrierName) === -1) {
console.warn("Warning: header '" + name + "' mentions a carrier that isn't in the carriers list.");
}
} else if (name.startsWith('X-')) {
//all extension header values are okay!
} else {
console.warn("Warning: header name '" + name + "' not recognized; header will still be written.");
}
this._headers.push(';;' + name + ': ' + value);
};
// escape hatch to dump your custom instruction to knitout
// if you know what you are doing
Writer.prototype.addRawOperation = function( operation ){
console.warn("Warning: operation added to list as is(string), no error checking performed.");
this._operations.push(operation);
};
//helpers to extract parameters from argument arrays:
// (these remove the extracted arguments from the start of the array and throw on errors)
//shiftDirection interprets the first element of 'args' as a direction, and throws on error:
// returns '+' or '-'.
function shiftDirection(args) {
console.assert(Array.isArray(args));
if (args.length === 0) {
throw new Error("Direction missing.");
}
if (!(args[0] === '+' || args[0] === '-')) {
throw new Error("Direction should be '+' or '-'.");
}
let dir = args.shift();
return dir;
}
//shiftBedNeedle interprets the first one or two arguments of 'args' as a bed+needle number, and throws on error:
// returns a {bed:, needle:} object.
const BedNeedleRegex = /^([fb]s?)(-?\d+)$/;
const BedRegex = /^[fb]s?$/;
function shiftBedNeedle(args) {
console.assert(Array.isArray(args));
if (args.length === 0) {
throw new Error("Needle missing.");
}
let bed, needle;
//case: bed string, needle number:
if (typeof(args[0]) === 'string' && BedRegex.test(args[0])) {
if (args.length < 2 || !Number.isInteger(args[1])) {
throw new Error("Expecting bed name to be followed by a needle number.");
}
bed = args.shift();
needle = args.shift();
//case: single string "f12", "b-2", "bs66":
} else if (typeof(args[0]) === 'string') {
let m = args[0].match(BedNeedleRegex);
if (m === null) {
throw new Error("String '" + args[0] + "' does not look like a compound bed+needle string.");
}
bed = m[1];
needle = parseInt(m[2]);
args.shift();
//case: two-member array ["fs", 2]
} else if (Array.isArray(args[0])) {
if (!( args[0].length === 2 && typeof(args[0][0]) === 'string' && BedRegex.test(args[0][0]) && Number.isInteger(args[0][1]) )) {
throw new Error("Bed+needle array should look like [\"f\", 12].");
}
bed = args[0][0];
needle = args[0][1];
args.shift();
//case: object {bed:"fs", needle:5}
} else if (typeof(args[0]) === 'object') {
if (!( 'bed' in args[0] && typeof(args[0].bed) === 'string' && BedRegex.test(args[0].bed) )) {
throw new Error("Bed+needle object should have a 'bed' member string naming the bed.");
}
if (!( 'needle' in args[0] && Number.isInteger(args[0].needle) )) {
throw new Error("Bed+needle object should have a 'needle' member integer.");
}
bed = args[0].bed;
needle = args[0].needle;
args.shift();
} else {
throw new Error("Expecting bed+needle as name+number (\"fs\", 6), string (\"b-2\"), array ([\"f\", 6]), or object ({bed:\"bs\", needle:12}). Got '" + JSON.stringify(args) + "'");
}
return {bed:bed, needle:needle};
}
//shiftCarrierSet interprets the remaining contents of 'args' as an array of carrier names, and throws on error:
// returns an array of carrier names, e.g., ["C", "A"].
function shiftCarrierSet(args, carrierNames) {
let carrierSet = [];
//carrier set as array, e.g., knit(..., ["A", "B"]):
if (args.length === 1 && Array.isArray(args[0])) {
carrierSet = args.shift().slice();
} else {
//carrier set as parameters, e.g., knit(..., "A", "B");
carrierSet = args.splice(0,args.length).slice();
}
// slightly ugly handling of various ways of typeing "A B", "A, B"
carrierSet.forEach(function(name, idx){
let space_split = name.split(" ");
let first = true;
space_split.forEach(function(s, sidx){
if(s == '') return;
if(first){
carrierSet[idx] = s;
first = false;
}
else carrierSet.push(s);
});
});
carrierSet.forEach(function(name, idx){
let comma_split = name.split(",");
let first = true;
comma_split.forEach(function(s, sidx){
if(s =='') return;
if(first) {
carrierSet[idx] = s;
first = false;
}
else carrierSet.push(s);
});
});
carrierSet.forEach(function(name){
if (carrierNames.indexOf(name) === -1) {
throw new Error("Invalid carrier name '" + name + "'");
}
});
return carrierSet;
}
Writer.prototype.in = function(...args){
let cs = shiftCarrierSet(args, this._carriers);
if (cs.length === 0) {
throw new Error("It doesn't make sense to 'in' on an empty carrier set.");
}
cs.forEach(function(cn){
if (cn in this.carriers) {
throw new Error("Carrier '" + cn + "' is already in.");
}
this.carriers[cn] = {hook:false};
}, this);
this._operations.push('in ' + cs.join(' '));
};
Writer.prototype.inhook = function(...args){
let cs = shiftCarrierSet(args, this._carriers);
if (cs.length === 0) {
throw new Error("It doesn't make sense to 'inhook' on an empty carrier set.");
}
cs.forEach(function(cn){
if (cn in this.carriers) {
throw new Error("Carrier '" + cn + "' is already in.");
}
this.carriers[cn] = {hook:true};
}, this);
this._operations.push('inhook ' + cs.join(' '));
};
Writer.prototype.releasehook = function(...args){
let cs = shiftCarrierSet(args, this._carriers);
if (cs.length === 0) {
throw new Error("It doesn't make sense to 'releasehook' on an empty carrier set.");
}
cs.forEach(function(cn){
if (!(cn in this.carriers)) {
throw new Error("Carrier '" + cn + "' isn't in.");
}
if (!this.carriers[cn].hook) {
throw new Error("Carrier '" + cn + "' isn't in the hook.");
}
this.carriers[cn].hook = false;
}, this);
this._operations.push('releasehook ' + cs.join(' '));
};
Writer.prototype.out = function(...args){
let cs = shiftCarrierSet(args, this._carriers);
if (cs.length === 0) {
throw new Error("It doesn't make sense to 'out' on an empty carrier set.");
}
cs.forEach(function(cn){
if (!(cn in this.carriers)) {
throw new Error("Carrier '" + cn + "' isn't in.");
}
delete this.carriers[cn];
}, this);
this._operations.push('out ' + cs.join(' '));
};
Writer.prototype.outhook = function(...args){
let cs = shiftCarrierSet(args, this._carriers);
if (cs.length === 0) {
throw new Error("It doesn't make sense to 'outhook' on an empty carrier set.");
}
cs.forEach(function(cn){
if (!(cn in this.carriers)) {
throw new Error("Carrier '" + cn + "' isn't in.");
}
delete this.carriers[cn];
}, this);
this._operations.push('outhook ' + cs.join(' '));
};
function isFiniteNumber( n ) {
if (typeof(n) === 'number' && Number.isFinite(n) && !Number.isNaN(n)) return true;
return false;
}
Writer.prototype.stitch = function(before, after) {
if (!(isFiniteNumber(before) && isFiniteNumber(after))) {
throw new Error("Stitch L and T values must be finite numbers.");
}
this._operations.push('stitch ' + before.toString() + ' ' + after.toString());
};
//throw warning if ;;Machine: header is included & machine doesn't support extension
function machineSupport(extension, supported) {
if (!machine.toUpperCase().includes(supported)) console.warn(`Warning: ${extension} is not supported on ${machine}. Including it anyway.`);
}
// --- extensions ---
Writer.prototype.stitchNumber = function (stitchNumber) {
if (!(Number.isInteger(stitchNumber) && stitchNumber >= 0)) {
throw new Error("Stitch numbers are non-negative integer values.");
}
this._operations.push('x-stitch-number ' + stitchNumber.toString());
};
Writer.prototype.fabricPresser = function (presserMode) {
machineSupport('presser mode', 'SWG');
if(presserMode === 'auto'){
this._operations.push('x-presser-mode auto');
}
else if(presserMode === 'on'){
this._operations.push('x-presser-mode on');
}
else if(presserMode === 'off'){
this._operations.push('x-presser-mode off');
}
else{
console.warn('Ignoring presser mode extension, unknown mode ' + presserMode + '. Valid modes: on, off, auto');
}
}
Writer.prototype.visColor = function (hex, carrier) {
let warning = false;
if (arguments.length !== 2) {
warning = true;
console.warn(`Ignoring vis color extension, since it is meant to take 2 parameters: 1) #hexColorCode and 2) carrierNumber.`);
}
if (hex.charAt(0) !== '#') {
warning = true;
console.warn(`Ignoring vis color extension, since the first arg is meant to be a hex code to assign the given carrier. Expected e.g. #FF0000`);
}
if (this._carriers.indexOf(carrier) === -1) {
warning = true;
console.warn(`Ignoring vis color extension, since the second arg is meant to be the carrier number to which you are assigning the color. ${carrier} is not listed in the 'Carriers' header.`);
}
if (!warning) this._operations.push(`x-vis-color ${hex} ${carrier}`);
}
Writer.prototype.speedNumber = function (value) {
//TODO: check to make sure it's within the accepted range
if (!(Number.isInteger(value) && value >= 0)) {
console.warn(`Ignoring speed number extension, since provided value: ${value} is not a non-negative integer.`);
} else this._operations.push(`x-speed-number ${value}`);
};
Writer.prototype.rollerAdvance = function (value) {
machineSupport('roller advance', 'KNITERATE');
//TODO: check to make sure it's within the accepted range
if (!Number.isInteger(value)) {
console.warn(`Ignoring roller advance extension, since provided value: ${value} is not an integer.`);
} else this._operations.push(`x-speed-number ${value}`);
let warning = false;
if (!warning) this._operations.push(`x-roller-advance ${value}`);
};
Writer.prototype.addRollerAdvance = function (value) {
machineSupport('add roller advance', 'KNITERATE');
//TODO: check to make sure it's within the accepted range
if (!Number.isInteger(value)) {
console.warn(`Ignoring add roller advance extension, since provided value: ${value} is not an integer.`);
} else this._operations.push(`x-add-roller-advance ${value}`);
};
Writer.prototype.carrierSpacing = function (value) {
machineSupport('carrier spacing', 'KNITERATE');
if (!(Number.isInteger(value) && value > 0)) {
console.warn(`Ignoring carrier spacing extension, since provided value: ${value} is not a positive integer.`);
} else this._operations.push(`x-carrier-spacing ${value}`);
};
Writer.prototype.carrierStoppingDistance = function (value) {
machineSupport('carrier stopping distance', 'KNITERATE');
if (!(Number.isInteger(value) && value > 0)) {
console.warn(`Ignoring carrier stopping distance extension, since provided value: ${value} is not a positive integer.`);
} else this._operations.push(`x-carrier-stopping-distance ${value}`);
};
// --- operations ---//
Writer.prototype.rack = function(rack) {
if (!(isFiniteNumber(rack))) {
throw new Error("Racking values must be finite numbers.");
}
this.racking = rack;
this._operations.push('rack ' + rack.toString());
};
Writer.prototype.knit = function(...args) {
let dir = shiftDirection(args);
let bn = shiftBedNeedle(args);
let cs = shiftCarrierSet(args, this._carriers);
if (cs.length > 0) {
this.needles[bn.bed + bn.needle.toString()] = true;
} else {
delete this.needles[bn.bed + bn.needle.toString()];
}
this._operations.push('knit ' + dir + ' ' + bn.bed + bn.needle.toString() + ' ' + cs.join(' '));
};
Writer.prototype.tuck = function(...args) {
let dir = shiftDirection(args);
let bn = shiftBedNeedle(args);
let cs = shiftCarrierSet(args, this._carriers);
this.needles[bn.bed + bn.needle.toString()] = true;
this._operations.push('tuck ' + dir + ' ' + bn.bed + bn.needle.toString() + ' ' + cs.join(' '));
};
Writer.prototype.split = function(...args) {
let dir = shiftDirection(args);
let from = shiftBedNeedle(args);
let to = shiftBedNeedle(args);
let cs = shiftCarrierSet(args, this._carriers);
if ((from.bed + from.needle.toString()) in this.needles) {
this.needles[to.bed + to.needle.toString()] = true;
delete this.needles[from.bed + from.needle.toString()];
}
if (cs.length > 0) {
this.needles[from.bed + from.needle.toString()] = true;
}
this._operations.push('split ' + dir + ' ' + from.bed + from.needle.toString() + ' ' + to.bed + to.needle.toString() + ' ' + cs.join(' '));
};
Writer.prototype.miss = function(...args) {
let dir = shiftDirection(args);
let bn = shiftBedNeedle(args);
let cs = shiftCarrierSet(args, this._carriers);
if (cs.length === 0) {
throw new Error("It doesn't make sense to miss with no carriers.");
}
this._operations.push('miss ' + dir + ' ' + bn.bed + bn.needle.toString() + ' ' + cs.join(' '));
};
// drop -> knit without yarn, but supported in knitout
Writer.prototype.drop = function(...args) {
let bn = shiftBedNeedle(args);
if (args.length !== 0) {
throw new Error("drop only takes a bed+needle");
}
delete this.needles[bn.bed + bn.needle.toString()];
this._operations.push('drop ' + bn.bed + bn.needle.toString());
};
// amiss -> tuck without yarn, but supported in knitout
Writer.prototype.amiss = function(...args) {
let bn = shiftBedNeedle(args);
if (args.length !== 0) {
throw new Error("amiss only takes a bed+needle");
}
this._operations.push('amiss ' + bn.bed + bn.needle.toString());
};
// xfer -> split without yarn, but supported in knitout
Writer.prototype.xfer = function(...args) {
let from = shiftBedNeedle(args);
let to = shiftBedNeedle(args);
if (args.length !== 0) {
throw new Error("xfer only takes two bed+needles");
}
if ((from.bed + from.needle.toString()) in this.needles) {
this.needles[to.bed + to.needle.toString()] = true;
delete this.needles[from.bed + from.needle.toString()];
}
this._operations.push('xfer ' + from.bed + from.needle.toString() + ' ' + to.bed + to.needle.toString());
};
// add comments to knitout
Writer.prototype.comment = function( str ){
let multi = str.split('\n');
multi.forEach(function(entry){
// cannot add header comments with comment
while(entry.startsWith(';')){
console.warn('Warning: comment starts with ; use addHeader for adding header comments.');
entry = entry.substr(1, entry.length);
}
this._operations.push(';' + entry.toString());
}, this);
};
Writer.prototype.pause = function(comment){
// deals with multi-line comments
this.comment(comment);
this._operations.push('pause');
};
Writer.prototype.write = function(filename){
let version = ';!knitout-2';
let content = version + '\n' +
this._headers.join('\n') + '\n' +
this._operations.join('\n');
if (typeof(filename) === 'undefined') {
console.warn("filename not passed to Writer.write; writing to stdout.");
console.log(content);
} else {
try{
let fs = require('fs');
fs.writeFileSync(filename, content + '\n'); //default is utf8
}
catch(e){
console.warn("Can't load 'fs'. Did not write file.");
}
}
return content;
};
// browser-compatibility
if(typeof(module) !== 'undefined'){
module.exports.Writer = Writer;
}
;