Any easy way to detect if a PathItem is a given shape?

Engaged ,
Jun 18, 2020 Jun 18, 2020

Copy link to clipboard

Copied

I'd like to iterate through PathItems and detect if one is a parametric shape like a rectangle, ellipse, polygon, etc., specifically having been made from the corresponding tool (rectangle tool, and so on). I figure this can be done by iterating through PathPoints of enclosed PathItems:

 

  • Rectangle has 4 vertices, and two pairs of equal paths
  • You could tell if something is an ellipse by looking at the distance of the Bezier handles or anchors. If these are all equal length, it's a perfect circle. It it has an equal amount of two matching pairs, it's oblong.
  • Polygons should have equal distance between all pathPoints, and would have 3 or more sides

 

So I figure I could do all this logic, but first was curious if there's a much easier way to detect since I'm essentially just looking to return a value of "Rectangle", "Ellipse", "Polygon". Is there any easier way to do this?

TOPICS
Scripting

Views

163

Likes

translate

Translate

Translate

Report

Report
Community Guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines

correct answers 1 Correct Answer

Adobe Community Professional , Jun 19, 2020 Jun 19, 2020
not easily as with most everything but there's hope   if you save your document with "Use Compression" unchecked, and open the ai document with a Text Editor, you'll find a lot of useful information about the file.   search for "BeginLayer" to get to the good stuff. It turns out Illustrator adds tags to Shapes   this snippet shows a 6 side Polygone named "myHexagone". Expanded Shapes just list a bunch of Anchor coordinates, no shape tags  

Likes

translate

Translate

Translate
Adobe Community Professional ,
Jun 19, 2020 Jun 19, 2020

Copy link to clipboard

Copied

not easily as with most everything but there's hope

 

if you save your document with "Use Compression" unchecked, and open the ai document with a Text Editor, you'll find a lot of useful information about the file.

 

search for "BeginLayer" to get to the good stuff. It turns out Illustrator adds tags to Shapes

 

this snippet shows a 6 side Polygone named "myHexagone". Expanded Shapes just list a bunch of Anchor coordinates, no shape tags

 

shapeObjectsUncompressedFile.PNG

Likes

translate

Translate

Translate

Report

Report
Community Guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Engaged ,
Nov 15, 2020 Nov 15, 2020

Copy link to clipboard

Copied

Hey Carlos, I wanted to come back to this due to that renaming script from earlier this week.

 

Is the Use Compression option accessible via scripting in any way? Is it possible to programmatically save an uncompressed file to later parse, or expand a compress file to get the same result as Use Compression = false?

Likes

translate

Translate

Translate

Report

Report
Community Guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Engaged ,
Nov 15, 2020 Nov 15, 2020

Copy link to clipboard

Copied

For reference if any one sees this in the future and was wondering how to do what I initially suggested with detecting (pure) parametric shapes, this is pretty simple but it doesn't handle the fact that Illustrator will name "<Rectangle>" for something generated from the Rectangle tool and this doesn't change even after modifying the points, or how using the Polygon tool to create a 4 sided polygon will result in "<Polgyon>".

Example code:

// The layer to check items of:
var targetLayer = app.activeDocument.layers[1];
// To prevent small rounding errors, use Math.round() on anchors, handles, and lengths
var settings = {
  roundIntegers: true,
};
//
// Adding a few helpers and polyfills here which need to be ran prior to invoking main function
//
// Polyfill for Math.hypot, to later find the distance between two points
if (!Math.hypot)
  Math.hypot = function () {
    var max = 0;
    var s = 0;
    var containsInfinity = false;
    for (var i = 0; i < arguments.length; ++i) {
      var arg = Math.abs(Number(arguments[i]));
      if (arg === Infinity) containsInfinity = true;
      if (arg > max) {
        s *= (max / arg) * (max / arg);
        max = arg;
      }
      s += arg === 0 && max === 0 ? 0 : (arg / max) * (arg / max);
    }
    return containsInfinity
      ? Infinity
      : max === 1 / 0
      ? 1 / 0
      : max * Math.sqrt(s);
  };
//
// Polyfills for Array methods to make iterating over and working with data much easier
Array.prototype.forEach = function (callback) {
  for (var i = 0; i < this.length; i++) callback(this[i], i, this);
};
Array.prototype.includes = function (item) {
  for (var i = 0; i < this.length; i++) if (this[i] == item) return true;
  return false;
};
Array.prototype.filter = function (callback) {
  var filtered = [];
  for (var i = 0; i < this.length; i++)
    if (callback(this[i], i, this)) filtered.push(this[i]);
  return filtered;
};
Array.prototype.map = function (callback) {
  var mappedParam = [];
  for (var i = 0; i < this.length; i++)
    mappedParam.push(callback(this[i], i, this));
  return mappedParam;
};
// Helper for converting DOM collections to native Arrays
function get(type, parent, deep) {
  if (arguments.length == 1 || !parent) {
    parent = app.activeDocument;
    deep = true;
  }
  var result = [];
  if (!parent[type]) return [];
  for (var i = 0; i < parent[type].length; i++) {
    result.push(parent[type][i]);
    if (parent[type][i][type] && deep)
      result = [].concat(result, get(type, parent[type][i]));
  }
  return result || [];
}
//
//
// The real code:
//
function checkDocItems() {
  // Get either selection or a specified Layer
  var list = get(
    app.selection.length ? app.selection : "pathItems",
    targetLayer,
    false
  );
  // Make sure this includes only PathItems and not CompoundPaths
  list = list.filter(function (item) {
    return !/compound/i.test(item.typename) && /path/i.test(item.typename);
  });
  // Pass to a utility function and display the result
  list.forEach(function (item) {
    var isParametric = isParametricShape(item, settings);
    alert(item.name + " == " + isParametric);
  });
}

function isParametricShape(item, options) {
  var result = false;
  var pointsList = get("pathPoints", item);
  // If open shape, no points, or less than 3 points it can't be parametric:
  if (!item.closed || !pointsList.length || pointsList.length < 3) return false;
  // Otherwise collect data about every point:
  var pointsDetails = pointsList.map(function (point, index, list) {
    var prevIndex = index < 1 ? list.length - 1 : index - 1;
    var nextIndex = (index + 1) % list.length;
    var nextPoint = list[nextIndex];
    var chain = [list[prevIndex].anchor, point.anchor, list[nextIndex].anchor];
    var handles = {
      leftLength: Math.hypot(
        point.leftDirection[0] - point.anchor[0],
        point.leftDirection[1] - point.anchor[1]
      ),
      rightLength: Math.hypot(
        point.rightDirection[0] - point.anchor[0],
        point.rightDirection[1] - point.anchor[1]
      ),
      totalLength: 0,
      exists: false,
    };
    handles.totalLength = settings.roundIntegers
      ? Math.round(handles.rightLength + handles.leftLength)
      : handles.rightLength + handles.leftLength;
    if (handles.leftLength || handles.rightLength) handles.exists = true;
    return {
      index: index,
      anchor: point.anchor,
      angle: getAngleFromChain(chain),
      length: Math.hypot(
        nextPoint.anchor[0] - point.anchor[0],
        nextPoint.anchor[1] - point.anchor[1]
      ),
      handles: handles,
    };
  });

  var totalAngles = [],
    totalLengths = [],
    totalHandles = [],
    totalPoints = [];
  pointsDetails.forEach(function (detail) {
    // Round integers when needed and add new instances to a specific Array per detail:
    if (settings.roundIntegers)
      (detail.angle = Math.round(detail.angle)),
        (detail.length = Math.round(detail.length));
    if (!totalAngles.includes(detail.angle)) totalAngles.push(detail.angle);
    if (!totalLengths.includes(detail.length)) totalLengths.push(detail.length);
    if (
      !totalHandles.includes(detail.handles.totalLength) &&
      detail.handles.totalLength !== 0
    )
      totalHandles.push(detail.handles.totalLength);

    totalPoints.push(detail.anchor);
  });
  var deduction = getDeduction(
    totalPoints,
    totalAngles,
    totalLengths,
    totalHandles
  );
  var resultString =
    item.name +
    " isParametric: " +
    deduction.isParametric +
    "\r\n" +
    deduction.type +
    " == " +
    deduction.name +
    "\r\nTotal angles: " +
    totalAngles.length +
    "\r\nTotal lengths: " +
    totalLengths.length;
  alert(resultString);
  return deduction.isParametric;
}

function getDeduction(points, angles, lengths, handles) {
  var result = {
    isParametric: false,
    type: "Path",
    name: "Unknown",
  };
  if (
    points.length == 4 &&
    angles.length == 1 &&
    lengths.length == 1 &&
    handles.length
  ) {
    result.isParametric = true;
    result.name = "Circle";
    result.type = "Ellipse";
  } else if (
    points.length == 4 &&
    angles.length == 1 &&
    lengths.length == 1 &&
    !handles.length
  ) {
    result.isParametric = true;
    result.name = "Square";
    result.type = "Rectangle";
  } else if (
    points.length == 4 &&
    angles.length == 1 &&
    lengths.length == 2 &&
    !handles.length
  ) {
    result.isParametric = true;
    result.name = "Rectangle";
    result.type = "Rectangle";
  } else if (
    points.length >= 3 &&
    angles.length == 1 &&
    lengths.length == 1 &&
    !handles.length
  ) {
    result.isParametric = true;
    result.type = "Polygon";
    result.name = getPolygonName(points.length);
  }
  return result;
}

function getPolygonName(length) {
  var names = [
    null,
    null,
    null,
    "Triangle",
    "Rectangle",
    "Pentagon",
    "Hexagon",
    "Heptagon",
    "Octagon",
    "Nonagon",
    "Decagon",
  ];
  return names[length];
}

// Return the number of degrees from 3 connected points:
function getAngleFromChain(chain) {
  var A = chain[0];
  var B = chain[1];
  var C = chain[2];
  var AB = Math.sqrt(Math.pow(B[0] - A[0], 2) + Math.pow(B[1] - A[1], 2));
  var BC = Math.sqrt(Math.pow(B[0] - C[0], 2) + Math.pow(B[1] - C[1], 2));
  var AC = Math.sqrt(Math.pow(C[0] - A[0], 2) + Math.pow(C[1] - A[1], 2));
  return (
    Math.acos((BC * BC + AB * AB - AC * AC) / (2 * BC * AB)) * (180 / Math.PI)
  );
}

// Initiate the main function
checkDocItems();

Likes

translate

Translate

Translate

Report

Report
Community Guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines