0 Replies Latest reply on Nov 5, 2017 10:18 AM by Silly-V

    'Conditional Distance' algorithm

    Silly-V Adobe Community Professional

      I made an algorithm to test a "conditional distance". The goal is to obtain a location point along a set of values which signify a point close to the threshold between when a condition is valid and when it is not.

      Traditionally, in manipulating xy coordinates, we have knowledge of locations of art items in order to determine distance between the two. However there are some instances of work where this kind of location cannot be known. A widely used example is overset text: we have no way of knowing at what value the text becomes overset until the font size value is put in place and some Illustrator code can determine if the text is indeed overset.

      This also is an issue in photoshop scripting when one wishes to obtain the location of a boundary between a color and a transparent zone within a row or column of pixels.

      The main challenge is to perform the conditional tests as few times as possible while getting the closest location value as the result. For example, when sizing Illustrator font sizes or traversing photoshop pixels it becomes very time consuming to change the location and check the conditions (in photoshop it's laying down a color sampler to obtain the pixel) when the increment is a linear value such as 1 font point or 1 pixel.

      Therefore I made this following algorithm with some trial and error involved and it seems to be working, never going over 20 steps and keeping most steps to 6-16.

       

      Can anyone help improve it and work out any bugs one may find?

       

      The function is supposed to be context-agnostic and I made sure it's working in the scenarios of moving XY coordinates, overset text and photoshop pixel reading.

       

      var ConditionalDistance = {
          checkPrecision: function(num, tolerance, target) {
              if (num - tolerance <= target) {
                  return true;
              }
              return false;
          },
          stepLog: [],
          currentLocation: null,
          quickView: function(msg, title) {
              if (title == undefined) {
                  title = '';
              }
              var w = new Window('dialog', title);
              var e = w.add('edittext', undefined, msg, {
                  multiline: true,
                  readonly: true
              });
              e.size = [700, 500];
              var okbtn = w.add('button', undefined, 'Ok');
              okbtn.onClick = function() {
                  w.close();
              }
              w.show();
          },
          getPrecisionLocation: function(conditionFunction, setLocationFunction, overUnder, precision, tolerance, initLocation, initStep, min, max) {
              /*
      conditionFunction: should test for a condition which observes a threshold between true/false values (greater/less than), not a single comparison to a value.
      A single comparison is ok though when it is a proxy for a mechanism which still presents this threshold: for example checking Illustrator overset text condition.
      setLocationFunction: when defined, this function manipulates a document object to be positioned or styled a certain way according to a new value passed in
      overUnder: when true, will return closest location where the conditionFunction result is true, vice versa if false
      precision: how small does the stepSize need to be in order to satisfy the precision requirement?
      tolerance: extra tolarance buffer for the precision, if needed
      initLocation: where the starting location is
      initStep: the initial stepSize
      min: optional minimum range number
      max: optional maximum range number
      In photoshop, laying down a pixel sampler beyond document bounds will result in a error. Defining min/max constrains the location to prevent this.
      In Illustrator overset text, it may be necessary to never go above or below certain font size in the checks
      */
              var stepSize, lastOverLocation, lastUnderLocation, smallestOverLocation, largestUnderLocation, currentLocation, polarity;
      
      
              precision = precision;
              stepSize = initStep;
              currentLocation = initLocation;
              this.currentLocation = currentLocation;
      
      
              if (conditionFunction()) { // first test of the condition: if it's true then the location is over the threshold
                  lastOverLocation = initLocation;
                  polarity = -1;
              } else {
                  lastUnderLocation = initLocation;
                  polarity = 1;
              }
              if (typeof tolerance == "number") {
                  tolerance = 0;
              }
              var lastPolarity = polarity; // keeps track of forward/backward direction
              var counter = 1; // amound of times gone back & forth
              var stepCounter = 0; // amount of actual steps of changes to the location
      
      
              while (!this.checkPrecision(stepSize, tolerance, precision)) { // perform this until the stepSize is within an acceptable range to the precision
                  do {
                      if (lastUnderLocation == currentLocation) {
                          if (largestUnderLocation == null) {
                              largestUnderLocation = lastUnderLocation;
                          }
                          if (lastUnderLocation > largestUnderLocation) {
                              largestUnderLocation = lastUnderLocation;
                          }
                      }
                      if (lastOverLocation == currentLocation) {
                          if (smallestOverLocation == null) {
                              smallestOverLocation = lastOverLocation;
                          }
                          if (lastOverLocation < smallestOverLocation) {
                              smallestOverLocation = lastOverLocation;
                          }
                      }
                      if (largestUnderLocation != null && smallestOverLocation != null) { // when the nearest over/under locations are known, stepSize becomes a fraction of this range
                          stepSize = (smallestOverLocation - largestUnderLocation);
                      }
                      if (counter % 2 != 0) { // if the location is still under, and the routine has already traversed a direction, the stepSize is multipled by some calculation
                          stepSize *= (2 * (1 / counter));
                      } else {
                          stepSize /= (2 * (1 + (1 / counter)));
                      }
                      var newLocation = currentLocation + (polarity * (stepSize));
                      var tempLocation = currentLocation;
                      if (typeof max != "undefined" || typeof(min) != "undefined") { // test the max/min and return as new closest end point either max or min
                          if (typeof max == "number") {
                              if (newLocation > max) {
                                  newLocation = max;
                                  tempLocation = max;
                                  if (smallestOverLocation == null || smallestOverLocation > max) {
                                      smallestOverLocation == max;
                                  }
                              }
                          }
                          if (typeof min == "number") {
                              if (newLocation < min) {
                                  newLocation = min;
                                  tempLocation = min;
                                  if (largestUnderLocation == null || largestUnderLocation < min) {
                                      largestUnderLocation == min;
                                  }
                              }
                          }
                          if (newLocation == min || newLocation == max) {
                              currentLocation = newLocation;
                              this.currentLocation = currentLocation;
                              counter++;
                              polarity *= -1;
                          }
                      }
      
                      currentLocation = newLocation;
                      // when a new location is set, add to log
                      this.stepLog.push("Step #" + (stepCounter + 1) + "\tLocation:" + currentLocation + ", Step Size:" + stepSize);
                      if (this.checkPrecision(stepSize, tolerance, precision)) {
                          this.quickView(this.stepLog.join("\n"));
                          if (overUnder) {
                              return lastOverLocation;
                          } else {
                              return lastUnderLocation;
                          }
                      }
                      if (typeof setLocationFunction == "function") {
                          setLocationFunction(currentLocation);
                      }
                      this.currentLocation = currentLocation;
                      stepCounter++;
                      if (!conditionFunction()) { // after the step is performed, the condition is checked again
                          lastUnderLocation = currentLocation;
                          if (lastOverLocation == tempLocation) { // if condition was negative and the last upper end-point is also the tempLocation, polarity (direction) is switched
                              polarity *= -1;
                          }
                      } else {
                          lastOverLocation = currentLocation;
                          if (lastUnderLocation == tempLocation) { // if condition was positibe and the last lower end-point is also the tempLocation, switch direction
                              polarity *= -1;
                          }
                      }
                  } while (polarity == lastPolarity); // repeat until direction is switched
                  counter++; // add to counter every time direction is switched
                  lastPolarity = polarity;
              }
              this.quickView(this.stepLog.join("\n")); // for testing purposes: show the steps
              if (overUnder) { // return the nearest located end-point based on the overUnder requirement
                  return lastOverLocation;
              } else {
                  return lastUnderLocation;
              }
          }
      };
      

       

      Here is how it is tested:

      Illustrator shape-moving test

      In an Illustrator document there's a circle called "puck" and a line path called "goal".

      #target illustrator
      function test() {
      #include "ConditionalDistance.jsx"
          function centerOf(item) {
              return [item.left + item.width / 2, item.top + item.height / 2];
          };
      
          var doc = app.activeDocument;
          var goal = doc.pathItems.getByName("goal");
          var puck = doc.pathItems.getByName("puck");
      
          function movePuck(newValue) {
              puck.left = newValue - (puck.width / 2);
          };
      
          function isPuckOverGoalLine() {
              return (centerOf(goal)[0] <= centerOf(puck)[0]);
          };
      
          var closestDistance = ConditionalDistance.getPrecisionLocation(isPuckOverGoalLine, movePuck, true, 1, 0.25, centerOf(puck)[0], doc.width / 10);
          movePuck(closestDistance);
      };
      test();
      

       

      Technically there's no need to put the movePuck function inside as a setLocationFunction because in this case it's not necessary to manipulate actual art to get a new condition test- as opposed to the overset text testing. So the one-time movePuck at the end would suffice. The 'overUnder' flag is set to 'true' and the returned value is going to be the closest value over the goal line.

      Screen Shot 2017-11-05 at 12.02.07 PM.pngScreen Shot 2017-11-05 at 12.03.37 PM.pngScreen Shot 2017-11-05 at 12.03.45 PM.png

      Illustrator overset text

      In an Illustrator document a selected textFrame with some text content needs to be selected.

       

       

      #target illustrator
      function test() {
      #include "ConditionalDistance.jsx"
      
          var doc = app.activeDocument;
          if (doc.selection == null || doc.selection.length == 0) {
              alert("No selection present.");
              return;
          }
          // set up a file with a text box
          var textBox = doc.selection[0];
      
          function setFontSize(newValue) {
              textBox.textRange.characterAttributes.size = newValue;
          };
      
          function isTextOverset() {
              if (textBox.lines.length > 0) {
                  if (textBox.lines[0].characters.length < textBox.characters.length) {
                      return true;
                  } else {
                      return false;
                  }
              } else if (textBox.characters.length > 0) {
                  return true;
              }
          };
          // false to get the nearest 'under' value
          var startingSize = textBox.textRange.characterAttributes.size;
          var closestFontSize = ConditionalDistance.getPrecisionLocation(isTextOverset, setFontSize, false, 0.2, 0, startingSize, startingSize / 10);
          textBox.textRange.characterAttributes.size = closestFontSize;
      };
      test();
      

       

      The 'overUnder' flag is set to false because the objective is to obtain the closest value under the overset threshold.

      Screen Shot 2017-11-05 at 12.08.07 PM.pngScreen Shot 2017-11-05 at 12.08.15 PM.pngScreen Shot 2017-11-05 at 12.08.23 PM.pngScreen Shot 2017-11-05 at 12.08.33 PM.pngScreen Shot 2017-11-05 at 12.08.44 PM.png

       

      Photoshop pixel edge

      In a Photoshop document a color area takes up a portion of the document as a rectangle fill which is less than the document height - leaving a section of transparent pixels.

      #target photoshop
      function test() {
      #include "ConditionalDistance.jsx"
      
          function getPixelInfo(x, y) {
              /* function to use a colorSampler to sample color at a specified pixel, returns color information based on a subset of available document modes */
              var doc = app.activeDocument;
              for (var i = 0; i < doc.colorSamplers.length; i++) {
                  doc.colorSamplers[i].remove();
              }
              var sampler = doc.colorSamplers.add([x, y]);
              try {
                  sampler.color;
              } catch (e) {
                  return "transparent";
              }
              var result, rounding = 0;
              if (doc.mode == DocumentMode.CMYK) {
                  result = "cmyk(" + (sampler.color.cmyk.cyan).toFixed(rounding) + ", " + sampler.color.cmyk.magenta.toFixed(rounding) + ", " + sampler.color.cmyk.yellow.toFixed(rounding) + ", " + sampler.color.cmyk.black.toFixed(rounding) + ")";
              } else if (doc.mode == DocumentMode.RGB) {
                  result = "rgb(" + sampler.color.rgb.red.toFixed(rounding) + ", " + sampler.color.rgb.green.toFixed(rounding) + ", " + sampler.color.rgb.blue.toFixed(rounding) + ")";
              } else if (doc.mode == DocumentMode.GRAYSCALE) {
                  result = "grayscale(" + sampler.color.gray.gray.toFixed(rounding) + ")";
              } else {
                  alert(doc.mode + ": getPixelInfo not implemented");
                  result = null;
              }
              doc.colorSamplers[0].remove();
              return result;
          };
      
          var doc = app.activeDocument;
          var horzPoint = Math.round(doc.width.value / 2);
      
          function isPixelTransparent() {
              return getPixelInfo(horzPoint, ConditionalDistance.currentLocation) == "transparent";
          };
      
          var closestDistance = ConditionalDistance.getPrecisionLocation(isPixelTransparent, undefined, true, 1, 0.25, 1, doc.height.value / 10, 0, doc.height.value - 1);
      };
      test();
      

      Screen Shot 2017-11-05 at 12.12.34 PM.pngScreen Shot 2017-11-05 at 12.13.08 PM.png