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

# 'Conditional Distance' algorithm

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,
});
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.

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) {
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.

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();
}
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();
```