The code generated by the Illustrator script & the Illustrator script ( CS5+ ) lies below
// the generated led colors from above dots pathItem Y/N
[[0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0],[255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255]]
// the dots coords, for debug ( & also to add in canvas script to try generating shortest path between dots )
[[38.6038818359375,-28.5126953125],[53.5712890625,-20.8525390625],[67.61376953125,-23.0546875],[83.61376953125,-28.2216796875],[99.447265625,-23.0546875],[113.6318359375,-20.9112854003906],[128.481628417969,-28.4538269042969],[128.457397460938,-47.0709228515625],[113.490051269531,-54.731201171875],[99.44775390625,-52.5283203125],[83.447509765625,-47.361328125],[67.61376953125,-52.5283203125],[53.4285278320312,-54.671875],[38.5792846679688,-47.12939453125]]
Illustrator script, where logic is:
order of dots items on dots layer in .ai file determines generated colors indices
for each mouth layer, produce an array of color indices
/*
Generate a mouthsLedsArrays.json from the shapes in layers other than the top one in the currently opened Illustrator document
*/
// ==== INCLUDES ====
[#include](https://forum.espruino.com/search/?q=%23include) "json2.js" // jshint ignore:line
// ==== ILLUSTRATOR FLAGS ====
app.coordinateSystem = CoordinateSystem.ARTBOARDCOORDINATESYSTEM; // use artboard-based coords ( needed to easily get noc_x & noc_y )
// ==== PNP HELPER ====
// To digg: https://stackoverflow.com/questions/22521982/check-if-point-inside-a-polygon
// also:http://alienryderflex.com/polygon/
function inside(point, vs) {
// ray-casting algorithm based on
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
var x = point[0], y = point[1];
var inside = false;
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
var xi = vs[i][0], yi = vs[i][1];
var xj = vs[j][0], yj = vs[j][1];
var intersect = ((yi > y) != (yj > y))
&& (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
};
// ==== SHORTEST COMBO HELPER ====
function perm(xs) {
var ret = [];
for (var i = 0; i < xs.length; i = i + 1) {
var rest = perm(xs.slice(0, i).concat(xs.slice(i + 1)));
if(!rest.length) {
ret.push([xs[i]])
} else {
for(var j = 0; j < rest.length; j = j + 1) {
ret.push([xs[i]].concat(rest[j]))
}
}
}
return ret;
}
//console.log(perm([0,1,2]).join("\n"));
/*
alternative timed permutator
function permutator(inputArr) {
var results = [];
function permute(arr, memo) {
var cur, memo = memo || [];
for (var i = 0; i < arr.length; i++) {
cur = arr.splice(i, 1);
if (arr.length === 0) {
results.push(memo.concat(cur));
}
permute(arr.slice(), memo.concat(cur));
arr.splice(i, 0, cur[0]);
}
return results;
}
return permute(inputArr);
}
var t0 = performance.now(); //console.time('someFunction');
//var res = permutator([0,1,2,3,4,5,6,7,8]); // ~ 1191.80ms
//var res = permutator([0,1,2,3,4,5,6,7,8,9]); // ~12969.20ms
var res = permutator([0,1,2,3,4,5,6,7,8,9,10]); // ~ 147567.70 mins
console.log(res.length);
var t1 = performance.now(); // console.timeEnd('someFunction');
console.log("Call to doSomething took " + (t1 - t0).toFixed(2) + " milliseconds.")
*/
// ==== UNITS HELPERS ====
//function rndMm(num){ return ( num * 0.3528 ).toFixed(2); } // quick helper - getting mm
function rndMm(num){ return ( num ).toFixed(2); } // quick helper - getting pixels
// -= STEPS =-
// - 0: for each mouth shape on the 'template' ( top-most ) layer, position one ( or more equally distant ) 'dot(s)' on the 'dots' layer
// - 1: get the second-most layer ( the 'dots' one )
// - 2: get all of its 'dots' pathItems in an array
// - 3: create a 'maskMouthsArray', and for [2..n] other ( down from second ) layers, create a 'mouthArray' and:
// ---- 4: for each dot on the 'dots' layer:
// ------- 5: for evey shape in layer n:
// ---------- 6: if shape 'contains' the dot, either set our current mouthArray[dot] to '1' or directly to '255, 255, 255' ( for direct RGB White mapping )
// - 7: push our current 'mouthArray' to our 'maskMouthsArray'
// - 8: once all layers have been processed, save the 'maskMouthsArray' in the 'mouthsLedsArrays.json' output file
// -= QUICK IMPLM =-
var layers = app.activeDocument.layers;
// - 0: for each mouth shape on the 'template' ( top-most ) layer, position one ( or more equally distant ) 'dot(s)' on the 'dots' layer [TODO]
var templateLayer = layers[0];
alert('templateLayer: '+ templateLayer.name);
// - 1: get the second-most layer ( the 'dots' one )
var dotsLayer = layers[2];
alert('dotsLayer: '+ dotsLayer.name);
// - 2: get all of its 'dots' pathItems ( an array )
var dots = dotsLayer.pathItems;
// ---- BRUTE-FORCE OPTIMAL DOTS WIRING ORDER: start ----
var dotsCoordsArr = [];
// - 2.1: get array of the coords of the dots
for(var dotI = 0; dotI < dots.length; dotI++){
var dotC = dots[dotI].position;
dotC[0] += dots[dotI].width/2.0; // dotCx
dotC[1] -= dots[dotI].height/2.0; // dotCy
dotsCoordsArr.push([ dotC[0], dotC[1] ]);
}
/*
// - 2.2: get all possible permutations of their indices ( from 0 to length-1 )
var dotsIndices = [];
for(var dotI = 0; dotI < dots.length; dotI++){ dotsIndices.push(dotI); } // get every possible index
// GET ONLY FEW FIRST DOTS TO TRY GOING THROUGH WHILE NOT FREEZING ??
//dotsIndices = dotsIndices.slice(0, 4); // get 4 dots out of n ( 14 in our example ) -> gives 24 possible permutations
dotsIndices = dotsIndices.slice(0, 8);
var possibleIndicesPermutations = perm(dotsIndices);
// --> THE LINE ABOVE IS WHAT MAKES ILLUSTRATOR JS INTERPRETER FREEZE WHEN APPLIED TO AN ARRAY OF 14 ITEMS
// considering for [0..8] we'd get 362880 possible permutations
alert('possible permutations: ' + possibleIndicesPermutations.length);
//* --> the following make Illustrator no longer responsive .. aka crash ;/
// - 2.3: for all permutations, sum the distances between i & i+1 for each permutation
var summedDistances = [];
for(var dotPermsI = 0; dotPermsI < possibleIndicesPermutations.length; dotPermsI++){ // for each permutation
var summedDists = 0;
var currPerm = possibleIndicesPermutations[dotPermsI];
//alert('current permutation: ' + JSON.stringify(currPerm) );
//for(var dotPermI; dotPermI < possibleIndicesPermutations[dotPermsI].length; dotPermI++ ){ // for each dot index in it
for(var dotPermI = 0; dotPermI < currPerm.length; dotPermI++ ){ // for each dot index in it
var currPermCurrDot = currPerm[dotPermI];
//alert('current permutation current dot idx: ' + JSON.stringify(currPerm[currPermCurrDot]) );
//alert('current permutation current dot idx: ' + JSON.stringify(currPermCurrDot) );
dots[currPermCurrDot].name = 'LOL'+currPermCurrDot;
// get distance between dot[i] & dot[i+1]
if(dotPermI < possibleIndicesPermutations[dotPermsI].length-1){ // beginning to end
var dotC = dots[currPermCurrDot].position;
dotC[0] += dots[currPermCurrDot].width/2.0; // dotCx
dotC[1] -= dots[currPermCurrDot].height/2.0; // dotCy
var dotC2 = dots[currPermCurrDot].position;
dotC2[0] += dots[currPermCurrDot].width/2.0; // dotC2x
dotC2[1] -= dots[currPermCurrDot].height/2.0; // dotC2y
var dist = Math.sqrt( Math.pow((dotC[0] - dotC2[0]), 2) + Math.pow((dotC[1] - dotC2[1]), 2) );
summedDists += dist;
} else { // end to beginning
var dotC = dots[currPermCurrDot].position;
dotC[0] += dots[currPermCurrDot].width/2.0; // dotCx
dotC[1] -= dots[currPermCurrDot].height/2.0; // dotCy
var dotC2 = dots[currPerm[0]].position;
dotC2[0] += dots[currPerm[0]].width/2.0; // dotC2x
dotC2[1] -= dots[currPerm[0]].height/2.0; // dotC2y
var dist = Math.sqrt( Math.pow((dotC[0] - dotC2[0]), 2) + Math.pow((dotC[1] - dotC2[1]), 2) );
summedDists += dist;
}
}
summedDistances.push(summedDists);
}
// - 2.4: check which of the sum is the lowest
var lowestSummedDistIdx = 0;
var lowestSummedDist = summedDistances[0]; // set as 1st permutation's sum to start
for(var sumDistI = 0; sumDistI < summedDistances.length; sumDistI++){
if(summedDistances[sumDistI] < lowestSummedDist){
lowestSummedDistIdx = sumDistI;
lowestSummedDist = summedDistances[sumDistI];
}
}
alert('best permutation total length: ' + lowestSummedDist);
// - 2.5: re-order thz z-index of the dots by looping over the indices in the permutation result that has the lowest sum,
// from end to beginning & doing 'bring to front' operation on i
var bestPermutation = possibleIndicesPermutations[lowestSummedDistIdx];
for (var i = bestPermutation.length - 1; i >= 0; --i) {
dots[bestPermutation[i]].zOrder(ZOrderMethod.BRINGTOFRONT);
}
*/
// ---- BRUTE-FORCE OPTIMAL DOTS WIRING ORDER: end ----
// DEBUG: for now, rename the dots based on their zIndexPosition
var debugLayer = layers[1];
var dotsPathPts = []; // used to draw one big path instead of many little ones
for(var dotI = 0; dotI < dots.length; dotI++){
dots[dotI].name = dotI; //dots[dotI].zOrderPosition;
// TODO: add txt right within dot with idx
//var pointTextRef = docRef.textFrames.add();
var pointTextRef = debugLayer.textFrames.add();
pointTextRef.contents = dotI; //"TextFrame #3";
pointTextRef.story.textRange.justification = Justification.CENTER;
pointTextRef.textRange.size = 5;
//var textColor_gray = new GrayColor();
//textColor_gray.gray = 70;
//pointTextRef.textRange.paragraphs[0].fillColor = textColor_gray;
var textColor_rgb = new RGBColor();
textColor_rgb.blue = 255;
pointTextRef.textRange.paragraphs[0].fillColor = textColor_rgb;
//pointTextRef.contents.fillColor = app.colors.item("Red");
var dotC = dots[dotI].position;
dotC[0] += dots[dotI].width/2.0; // dotCx
dotC[1] -= dots[dotI].height/2.0; // dotCy
pointTextRef.top = dotC[1]; //700;
pointTextRef.left = dotC[0]; //400;
//pointTextRef.selected = true;
//redraw();
// draw a line between those as well
if(dotI < dots.length-1){
// get nxt dot
var dotC2 = dots[dotI+1].position;
dotC2[0] += dots[dotI+1].width/2.0; // dotCx
dotC2[1] -= dots[dotI+1].height/2.0; // dotCy
dotsPathPts.push([ dotC[0], dotC[1] ], [ dotC2[0], dotC2[1] ]);
/*
var strokeColor = new RGBColor();
strokeColor.green = 255;
var line = debugLayer.pathItems.add();
line.stroked = true; // to be able to see it
line.strokeColor = strokeColor;
line.strokeWidth = 4 ;
line.setEntirePath( [ [ dotC[0], dotC[1] ], [ dotC2[0], dotC2[1] ] ] );
*/
}
}
var strokeColor = new RGBColor();
strokeColor.green = 255;
var line = debugLayer.pathItems.add();
line.stroked = true; // to be able to see it
line.filled = false;
line.strokeColor = strokeColor;
line.strokeWidth = 1;
//line.setEntirePath( [ [ dotC[0], dotC[1] ], [ dotC2[0], dotC2[1] ] ] );
line.setEntirePath( dotsPathPts );
redraw();
// - 3: create a 'maskMouthsArray', and for [2..n] other ( down from second ) layers, create a 'mouthArray' and:
var maskMouthsArr = [];
//var layerIdx= 2;
var layerIdx= 3; // take in account added "debug" layer ( where we trace debug lines & where dots numbers are displayed )
//for(layerIdx; layerIdx < layers.length -2; layerIdx++){ // process every layer except 1st & 2nd top-most
for(layerIdx; layerIdx < layers.length; layerIdx++){ // process every layer except 1st & 2nd top-most
alert('processing layer: '+ layers[layerIdx].name);
var mouthArr = [];
// ---- 4: for each dot on the 'dots' layer:
for(var dotIdx = 0; dotIdx < dots.length; dotIdx++){
// - 4.1.: getting the center of the dot
var dotC = dots[dotIdx].position;
dotC[0] += dots[dotIdx].width/2.0; // dotCx
dotC[1] -= dots[dotIdx].height/2.0; // dotCy
// ------- 5: for evey shape in layer n:
var nLayerShapes = layers[layerIdx].pathItems;
var dotShapeFound = false; // whether or not the dot has found a shape it fits in
for(var layerShapeIdx = 0; layerShapeIdx < nLayerShapes.length; layerShapeIdx++){
if(dotShapeFound == true) break; // don't loop further over other shapes since our current dot has found its shape mate
// else, continue looping until we deduce that the 'mouth section' corresponding to that dot is not used on this 'mouthArray'
// ---------- 6: if shape 'contains' the dot, either set our current mouthArray[dot] to '1' or directly to '255, 255, 255' ( for direct RGB White mapping )
/*
// --> HUGELY-IN-POC-AND-MAYBE-PROGRESS PART: bypassing the 'visibleBounds' & 'geometricBounds' limitations to check whether a dot is 'within' a shape <--
// - 6.0: getting the center of the shape
var c1 = nLayerShapes[layerShapeIdx].position;
c1[0] += nLayerShapes[layerShapeIdx].width/2.0;
//center[1] += obj.height/2.0;
c1[1] -= nLayerShapes[layerShapeIdx].height/2.0;
// - 6.1: change currently selected objs to have dots[dotIdx] & as selection for futher pathfinder bool op .. or just to 'group' those & apply the same idea ;)
dots[dotIdx].selected = true;
nLayerShapes[layerShapeIdx].selected = true;
// - 6.2: group those to see if this affect the shape's center by a yota or more
*/
// get pathPoints from shape ( R: currently, curves may produce weird results .. )
var shapePathPts = nLayerShapes[layerShapeIdx].pathPoints;
// loop over those & build an array of coords
var polygon = [];
for(var j=0; j < shapePathPts.length; j++){
var anch = shapePathPts[j].anchor; // its anchor
polygon.push([anch[0], anch[1]]);
}
//var polygon = [ [ 1, 1 ], [ 1, 2 ], [ 2, 2 ], [ 2, 1 ] ]; // array of coordinates of each vertex of the polygon
//inside([ 1.5, 1.5 ], polygon); // true - whether the test point in inside or outside the polygon
if( inside([ dotC[0], dotC[1] ], polygon) == true ){
dotShapeFound = true; // break outta loop
}
}
if(dotShapeFound == true){
mouthArr.push(255, 255, 255); // RGB LEDS like APA102 - full on ( white )
} else {
mouthArr.push(0, 0, 0); // RGB LEDS like APA102 - full off
}
}
// - 7: push our current 'mouthArray' to our 'maskMouthsArray'
// all dots have been processed for current mouth layer: push it to our maskMouths array
maskMouthsArr.push(mouthArr)
}
// - 8: once all layers have been processed, save the 'maskMouthsArray' in the 'mouthsLedsArrays.json' output file
//var container = { "expression": expression};
//alert(JSON.stringify(container) );
//File.saveDialog (prompt[, preset])
var jsonFile = new File("mouthsLedsArrays.json"); // def file name
var saveFilePath = jsonFile.saveDlg("Where to save generated stuff ? :)");
//var filepath = "~/Desktop/" + randomname + ".txt";
var write_file = File(saveFilePath);
if (!write_file.exists) {
// if the file does not exist create one
//write_file = new File(filepath);
write_file = new File(saveFilePath);
} else {
// if it exists ask the user if it should be overwritten
var res = confirm("The file already exists. Overwrite ?", true, "titleWINonly");
// if the user hits no stop the script
if (res !== true) {
//return; // threw an error on AI CS5 ..
write_file = '';
}
}
var out; // our output ( we know already that the file exist but to be sure )
if (write_file !== '') {
out = write_file.open('w', undefined, undefined); //Open the file for writing.
write_file.encoding = "UTF-8";
write_file.lineFeed = "Unix"; //convert to UNIX lineFeed
// txtFile.lineFeed = "Windows";
// txtFile.lineFeed = "Macintosh";
}
if (out !== false) { // got an output?
write_file.writeln( JSON.stringify(maskMouthsArr) ); // write generated stuff to json file
write_file.writeln( JSON.stringify(dotsCoordsArr) ); // debug coords array
write_file.close();
}
/*
// ==== EAGLE ILLUSTRATOR HELPER ====
//var eagleCenter = undefined;
//var eagleCenterPos = {};
// getting the items selected
var selecteditems = app.activeDocument.selection;
//alert("Found" + selecteditems.length + " selected items ..");
if(selecteditems.length < 1 ){ // || selecteditems.length > 2 ){
alert("Please select two elements, not less or more");
} else {
// start building our expression
var expression = {};
// get objects centers
var obj1 = selecteditems[0];
alert('nullObjectCenter -> obj.name: ' + obj1.name);
var c1 = obj1.position;
c1[0] += obj1.width/2.0;
//center[1] += obj.height/2.0;
c1[1] -= obj1.height/2.0;
expression.noc_x = parseFloat( rndMm( c1[0] ) );
//expression.noc_y = parseFloat( rndMm( c1[1] ) );
//expression.noc_y = parseFloat( rndMm( 64+ c1[1] ) );
expression.noc_y = parseFloat( rndMm( c1[1] * -1 ) );
/*
var obj2 = selecteditems[1];
var c2 = obj2.position;
c2[0] += obj2.width/2.0;
//center[1] += obj.height/2.0;
c2[1] -= obj2.height/2.0;
var objs = [
{cX: rndMm(c1[0]), cY: rndMm(c1[1])},
{cX: rndMm(c2[0]), cY: rndMm(c2[1])},
];
* /
expression.eyes = [];
// for each object selected after the 1st one
// /!\ R: the order is the STACKING order, not the one in which we selected objects
for(var i=1; i < selecteditems.length; i++){
var selectedItem = selecteditems[i];
alert(selectedItem.typename);
var pathPts = selectedItem.pathPoints;
//alert('pathPts: ' + selectedItem.pathPoints);
var polyPts = {};
// loop over those
//var cntr = 1;
for(var j=0; j < pathPts.length; j++){
var pathPt1 = pathPts[j]; // the point
var pt1Anch = pathPt1.anchor; // its anchor
var p1x = pt1Anch[0];
var p1y = pt1Anch[1];
/ *
// also get next points info ( to ease later parsing, we change the way path points are stored in the json to something quicker eagle-side )
var pathPt2 = pathPts[j+1]; // the next point
var pt2Anch = pathPt2.anchor; // its anchor
var p2x = pt2Anch[0];
var p2y = pt2Anch[1];
* /
// instead of adding those directly to our array holding the pathPts ready for Eagle, we compute their positions relative to our 'eagle_center'
//var relPt1x = parseFloat( rndMm( p1x - eagleCenterPos['cx'] ) );
//var relPt1y = parseFloat( rndMm( p1y - eagleCenterPos['cy'] ) );
//var relPt2x = parseFloat( rndMm( p2x - eagleCenterPos['cx'] ) );
//var relPt2y = parseFloat( rndMm( p2y - eagleCenterPos['cy'] ) );
var relPt1x = parseFloat( rndMm( p1x - c1[0] ) );
var relPt1y = parseFloat( rndMm( p1y - c1[1] ) );
// std pathPoint.anchor x & y
polyPts['x'+(j+1)] = relPt1x;
polyPts['y'+(j+1)] = relPt1y;
// DEBUG --
polyPts['X_POS'+(j+1)] = parseFloat( rndMm( p1x ) );
polyPts['Y_POS'+(j+1)] = parseFloat( rndMm( p1y ) );;
// DEBUG --
// quick test, to understand WHY & HOW I can't get bezier data out of shapes drawn using the pen tool ..
/ *
if(pathPt1.leftDirection && pathPt1.rightDirection )
alert('pathPt1.leftDirection exist on a point of that path, be it aof type "CORNER" or not ?! --> in: (' +
pathPt1.leftDirection[0] + ', ' + pathPt1.leftDirection[1] + ') out: (' +
pathPt1.rightDirection[0] + ', ' + pathPt1.rightDirection[1] + ') '
);
* /
// for smoothed stuff, handle in & out control points
// R: a rectangle or other shape becomes "SMOOTH" as soon as one of its point is modded ( ex: via pen tool )
// Q: are Illustrator actual cubic curves ? ( then we'd need to add the next anchor point as p3 - but we don't need to do so here -> so no storage overhead from that ;) )
if(pathPt1.pointType !== PointType.CORNER){ // other type is PointType.SMOOTH ( https://illustrator-scripting-guide.readthedocs.io/jsobjref/PathPoint/ )
polyPts['x'+(j+1)+'in'] = parseFloat( rndMm( pathPt1.leftDirection[0] - c1[0] ) );
polyPts['y'+(j+1)+'in'] = parseFloat( rndMm( pathPt1.leftDirection[1] - c1[1] ) );
polyPts['x'+(j+1)+'out'] = parseFloat( rndMm( pathPt1.rightDirection[0] - c1[0] ) );
polyPts['y'+(j+1)+'out'] = parseFloat( rndMm( pathPt1.rightDirection[1] - c1[1] ) );
}
// as a test
polyPts['x'+(j+1)+'pointType'] = pathPt1.pointType.toString().substr(pathPt1.pointType.toString().indexOf('.')+1);
}
expression.eyes.push(polyPts);
}
//alert(JSON.stringify(expression) );
var container = { "expression": expression};
alert(JSON.stringify(container) );
}
*/
Espruino is a JavaScript interpreter for low-power Microcontrollers. This site is both a support community for Espruino and a place to share what you are working on.
The code generated by the Illustrator script & the Illustrator script ( CS5+ ) lies below
Illustrator script, where logic is:
for each mouth layer, produce an array of color indices