Update Existing Path on Shape Layer in Photoshop UXP

The goal is to adjust a current path points of an existing shape layer.

I’m able to generate and add a new path (top left), and remove the old path, however the new path doesn’t have the same fill.

export const asModal = async (name: string, callback: Function) => {
  return await photoshop.core.executeAsModal(async () => await callback(), { name});
};

export const shapeTrial = () => {
  const points = [[0, 0],[0, 200],[200, 200],[200, 0]];  
  asModal("shape", async () => {
    let pointInfos = points.map((point) => {
      const psPoint = new photoshop.app.PathPointInfo();
      psPoint.anchor = point;
      psPoint.leftDirection = point;
      psPoint.rightDirection = point;
      psPoint.kind = photoshop.constants.PointKind.CORNERPOINT;
      return psPoint;
    });

    const spi = new photoshop.app.SubPathInfo();
    spi.closed = true;
    spi.operation = photoshop.constants.ShapeOperation.SHAPEXOR;
    spi.entireSubPath = pointInfos;

    photoshop.app.activeDocument.pathItems.add("box", [spi]);
  });
};

I think I need to update the points on the original path to preserve the fill, but I’m not seeing a way to via the API so far as the pathPoints object seems to be read-only.

photoshop.app.activeDocument.pathItems[0].subPathItems[0].pathPoints

Also tried Alchemist by @Jarda, but it doesn’t seem to record any relevant BatchPlay events when moving points around with the Direct Selection tool.

Did you try recording an action and then copying it as JS? That saved me a couple of times when Alchemist didn’t record anything

@Karmalakas do you have a Marketplace version of dev version from Github?

@justin2taylor you need to edit vector mask. Vector mask is not in DOM API.

From GitHub loaded via Dev Tools. I don’t remember for sure now what it was, but I think maybe switching between layer and mask view :thinking:

@Karmalakas Thanks, I tried recording an action, but just like Alchemist, nothing gets recorded when moving path points. Are there any other settings I should enable in order to capture vector path point movements?

action-record-path

@Jarda I just updated to the latest Alchemist version 2.7.0 on the GitHub master branch, that’s the latest correct? Still not seeing any relevant events when moving vector path points. I have all the types made visible, with Path selected. Is that the right strategy for trying to capture these sorts of actions?

alchemist-path

And thanks for calling out that this is a Vector Mask not a regular Mask/Path. Are you aware of any ways with BatchPlay to update Vector Mask Paths?


In other news, I also tried recording an Action with the old ScriptListener for ExtendScript, it also doesn’t capture anything that can playback. The following script just returns the error:

ps-extendscript-path-record.js (6.6 KB)

General Photoshop error occurred. This functionality may not be available in this version of Photoshop.
- The command "<unknown>" is not currently available.

You can’t record vector path movements… if you go for individual points. The API is not there. You can only set whole path all at once. That can’t be recorded either but it can be played.

Working with vector path reference is difficult. It has many quirks. You can try alchemist… and set dropdowns like this:

And it will give you reference like this

const {batchPlay} = require("photoshop").action;

const result = await batchPlay(
   [
      {
         _obj: "get",
         _target: [
            {
               _enum: "path",
               _ref: "path",
               _value: "vectorMask"
            },
            {
               _ref: "layer",
               _id: 2
            },
// optional document reference
            {
               _ref: "document",
               _id: 3420
            }
         ],
         _options: {
            dialogOptions: "dontDisplay"
         }
      }
   ],
   {}
);
1 Like

Thanks that helps.

I feel like I’m so close. I can get the points modify them, but just setting them with BatchPlay is still throwing the error:

The command "Set" is not currently available

const { executeAsModal } = photoshop.core;
const { batchPlay } = photoshop.action;

export const asModal = async (commandName, callback) => {
  return await executeAsModal(async () => await callback(), { commandName });
};

export const bpModal = async (commandName,  commands,  options) => {
  return await executeAsModal(
    async () => await batchPlay(commands, options || {}),
    { commandName }
  );
};


export const shapeBp = async (id) => {
  const res = await bpModal(
    "Get Info",
    [
      {
        _obj: "get",
        _target: [
          {
            _enum: "path",
            _ref: "path",
            _value: "vectorMask",
            _id: id,
          },
        ],
        _options: {
          dialogOptions: "dontDisplay",
        },
      },
    ],
    {}
  );
  console.log("bp res", res);
  return res[0];
};

export const setShapeBp = async (id, shapeObj) => {
  const res = await bpModal(
    "sdf",
    [
      {
        _obj: "set",
        _target: [
          {
            _ref: "path",
            _id: id,
          },
        ],
        to: shapeObj,
        _options: {
          dialogOptions: "dontDisplay",
        },
      },
    ],
    {}
  );
  console.log("new bp res", res);
};

export const modifyPath = async () => {
  const pathId = photoshop.app.activeDocument.pathItems[0].id;
  console.log("pathId", pathId);
  // Get Path
  let shape = await shapeBp(pathId);
  console.log("shape", shape);

  // Modify First Path Point
  shape.pathContents.pathComponents[0].subpathListKey[0].points[0].anchor.horizontal._value = 0;
  shape.pathContents.pathComponents[0].subpathListKey[0].points[0].anchor.vertical._value = 0;

  // set new path
  await setShapeBp(pathId, shape);
};

await modifyPath();

I think your references are wrong. Vector mask is enumurated reference… you can’t combine id and enum together. Also I think when you set it… you should not be able to set it by ID but rather by enum I think. And make sure that layer is selected and selecting the vector mask might help as well. You might also need to add layer reference into set action.

Tried a number of different configurations, ID-only, enum-only, setting the whole objects, setting only parts of the object. Closest I got was deleting the whole thing or else getting the same error: The command "Set" is not currently available

I went a different route however that has similar results. Instead of trying to get/set the VectorPoints object (e.g. Shape) I resorted to getting/setting the VectorMask object (e.g. Mask)

This seems to work given that you frequently correct the layer selection state, delete the old mask first with the BatchPlay script below (using a regular delete will delete the whole layer if there are other layers present)

await bpModal(
      "remove existing mask",
      [
        {
          _obj: "delete",
          _target: [
            {
              _ref: "path",
              _enum: "path",
              _value: "vectorMask",
            },
            {
              _ref: "layer",
              _enum: "ordinal",
              _value: "targetEnum",
            },
          ],
          _options: {
            dialogOptions: "dontDisplay",
          },
        },
      ],
      {}
    );

You then create the new Vector Path:

const res = await bpModal(
      "Create New Path",
      [
        {
          _obj: "set",
          _target: [
            {
              _ref: "path",
              _property: "workPath",
            },
            {
              _ref: "layer",
              _id: layer.id,
            },
          ],
          to: {
            _obj: "pathClass",
            pathComponents: [
              {
                _obj: "pathComponent",
                shapeOperation: {
                  _enum: "shapeOperation",
                  _value: "add",
                },
                subpathListKey: [
                  {
                    _obj: "subpath",
                    closedSubpath: true,
                    points: points,
                  },
                ],
              },
            ],
          },
          _options: {
            dialogOptions: "silent",
          },
        },
      ],
      {}
    );

And then set that new path as the vector mask for the layer:

await bpModal(
      "Set Mask",
      [
        {
          _obj: "make",
          _target: [
            {
              _ref: "path",
            },
            {
              _ref: "layer",
              _id: layer.id,
            },
          ],
          at: {
            _ref: "path",
            _enum: "path",
            _value: "vectorMask",
          },
          using: {
            _ref: "path",
            _enum: "ordinal",
            _value: "targetEnum",
          },
          _options: {
            dialogOptions: "dontDisplay",
          },
        },
      ],
      {}
    );

And then afterward, delete the duplicate path since it’s no longer needed:

    await asModal("remove duplicate path", async () => {
      const workPath = photoshop.app.activeDocument.pathItems.find(
        (i) => i.name === "Work Path"
      );
      if (workPath) {
        await workPath.remove();
      } else {
        console.warn("couldn't find work path, removing pathItems[1]");
        await photoshop.app.activeDocument.pathItems[1].remove();
      }
    });
2 Likes

If you delete path like that… it is possible it will work only in PS set to English lang.

Not sure, this project isn’t for the public so not too concerned, but is the main issue around using the term delete, that it would be localized if in a different locale?

It was the only way I could find to delete the last path without deleting the whole layer if more than 1 layer exists in the document. Interested if you know of any other ways.

Hi @justin2taylor,
Congrats for having found a workaround! :slight_smile: I believe @Jarda refers to the deletion of "Work Path", which in non-English locales may be a different string.
If it works for your use case, then who cares! :smiley:

Davide

1 Like

Ah right, yea that did occur to me in which case I’d just delete the path at index 1 if “Work Path” isn’t found. Tried to rely on IDs but for some reason both paths, active and duplicate, had the same ID, who knew :man_shrugging: