• 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) );
      }
      */
      
      
About

Avatar for stephaneAG @stephaneAG started