Undo / Revert to historyState in UXP

I had these two lines in my ExtendScript, that worked perfectly fine:
const initState = app.activeDocument.activeHistoryState;
app.activeDocument.activeHistoryState = initState;

They let me define a point in the history and then revert back to it anytime I wanted.
I am a bit confused about everything in UXP, though.

BingChatAI and ChatGPT first said that activeHistoryState is not supported in UXP anymore, then I find this page in the PS UXP documentary, which suggests, that the history is implemented, but then I find this page that suggests using batchPlay, which seems to work fine for the OP, but doesnt do anything for me (maybe because my function that uses that code is asynchonous?). Oh, and I found “await require(‘photoshop’).history.undo();”, which doesnt do anything either. And none of those things throw an error in the UXP console.

Would anybody please be so kindhearted to give me a little jumpstart on this? Thanks in advance!

Snap! I literally just posted code that does exactly this in another thread.

In amongst the rest of the code you’ll find 3 batchPlay helper functions makeSnapshot, revertToSnapshot, and deleteSnapshot.

That is really good timing of yours! :smiley:
Thank you very much for sharing your code, however I seem to be too dumb to implement it into my try…
Would you maybe be able to go the extra mile and tell me what’s wrong?
I now do not get any reaction when pushing a button, nor do I get any console log from the UXP developer tool. I am sorry for not being smarter.
I am sure its a really easy beginner’s mistake:

// #### helper functions ####
function createOutputFolders(projectfolder){
	try { new Folder(projectfolder + "09_Fuer_Exporter/936x676/").create(); } catch(e){}
	try { new Folder(projectfolder + "09_Fuer_Exporter/1602x2000/").create(); } catch(e){}
	try { new Folder(projectfolder + "09_Fuer_Exporter/2000x2000/").create(); } catch(e){}
}

function computeCropBounds(cropDirection, input_width, input_height, new_size){
	switch (cropDirection) {
	  // Crop to the top left corner
	  case "TL": var left = 0; var top = 0; var right = new_size[0]; var bottom = new_size[1]; break;
	  // Crop to the top horizontally centered
	  case "T": var left = (input_width - new_size[0]) / 2; var top = 0; var right = input_width - left; var bottom = new_size[1]; break;
	  // Crop to the top right corner
	  case "TR": var left = input_width - new_size[0]; var top = 0; var right = input_width; var bottom = new_size[1]; break;
	  // Crop to the left side, vertically centered
	  case "ML": var left = 0; var top = (input_height - new_size[1]) / 2; var right = new_size[0]; var bottom = input_height - top; break;
	  // Crop to center
	  case "M": var left = (input_width - new_size[0]) / 2; var top = (input_height - new_size[1]) / 2; var right = input_width - left; var bottom = input_height - top; break;
	  // Crop to the right side, vertically centered
	  case "MR": var left = input_width - new_size[0]; var top = (input_height - new_size[1]) / 2; var right = input_width; var bottom = input_height - top; break;
	  // Crop to the bottom left corner
	  case "BL": var left = 0; var top = input_height - new_size[1]; var right = new_size[0]; var bottom = input_height; break;
	  // Crop to the bottom - horizontally centered
	  case "B": var left = (input_width - new_size[0]) / 2; var top = input_height - new_size[1]; var right = input_width - left; var bottom = input_height; break;
	  // Crop to the bottom right corner
	  case "BR": var left = input_width - new_size[0]; var top = input_height - new_size[1]; var right = input_width; var bottom = input_height; break;
	}
	return [left, top, right, bottom]
}

async function cropDocument(cropL, cropT, cropR, cropB, output_width, output_height) {
	require("photoshop").app.activeDocument.crop({ left: cropL, top: cropT, right: cropR, bottom: cropB }, 0, output_width, output_height);
}

try {
  const makeSnapshot = async (name) => {
    return batchPlay(
      [
        {
          _obj: "make",
          _target: [
            {
              _ref: "snapshotClass",
            },
          ],
          from: {
            _ref: "historyState",
            _property: "currentHistoryState",
          },
          name: name,
          using: {
            _enum: "historyState",
            _value: "fullDocument",
          },
          _isCommand: true,
          _options: {},
        },
      ],
      {}
    );
  };

  const revertToSnapshot = async (name) => {
    return batchPlay(
      [
        {
          _obj: "select",
          _target: [
            {
              _ref: "snapshotClass",
              _name: name,
            },
          ],
          _isCommand: true,
          _options: {
            dialogOptions: "dontDisplay",
          },
        },
      ],
      {}
    );
  };

  const saveJPEG = async (saveFolder, newFilename, quality) => {
    try {
      const doc = app.activeDocument;
      const theNewFile = await saveFolder.createFile(newFilename, {
        overwrite: true,
      });
      const saveFile = await lfs.createSessionToken(theNewFile);
       return batchPlay(
        [
          {
            _obj: "save",
            as: {
              _obj: "JPEG",
              extendedQuality: quality,
              matteColor: {
                _enum: "matteColor",
                _value: "none",
              },
            },
            in: {
              _path: saveFile,
              _kind: "local",
            },
            documentID: doc.id,
            lowerCase: true,
            saveStage: {
              _enum: "saveStageType",
              _value: "saveBegin",
            },
            _isCommand: true,
            _options: {
              dialogOptions: "dontDisplay",
            },
          },
        ],
        {}
      );
    } catch (error) {
      console.log(error);
    }
  };
 } catch (error) {
   console.error(error);
   console.trace(error);
 }

// #### handle click events ####
function handleClick(cropDirection) {
(async () => {
	const app = require("photoshop").app;
	const batchPlay = require("photoshop").action.batchPlay;
	const lfs = require("uxp").storage.localFileSystem;
	var document = app.activeDocument;
	var input_width = document.width;
	var input_height = document.height;
	var filepath = String(document.path);
	var projectfolder =  filepath.substr(0, filepath.lastIndexOf("02_Capt_Export"));
	//alert(projectfolder);
	createOutputFolders(projectfolder);
	
	///////////////////////////////////////////////////////////////////// 936*676 /////////////////////////////////////////////////////////////////////
	await makeSnapshot("start");
	var factor = input_width / 936;
	var temp_height = 676 * factor;
	var new_size = [input_width, temp_height];

	if (temp_height > input_height){ // if input image is really wide, but not very tall, otherwise it would crop white onto the top and bottom
		factor = input_height / 676;
		temp_width = 936 * factor;
		new_size = [temp_width, input_height];
		}
	
	var cropBounds = computeCropBounds(cropDirection, input_width, input_height, new_size);
	require("photoshop").core.executeAsModal(() => cropDocument(cropBounds[0], cropBounds[1], cropBounds[2], cropBounds[3], 936, 676));
	await saveJPEG(projectfolder + "09_Fuer_Exporter/" + document.width + "x" + document.height + "/", document.name, 10);
	
	//////////////////////////////////////////////////////////////////// 1602*2000 ////////////////////////////////////////////////////////////////////
	await revertToSnapshot("start");
	// REVERT THE IMAGE
	var factor = input_height / 2000;
	var temp_width = 1602 * factor;
	var new_size = [temp_width, input_height];
	
	if (temp_width > input_width){ // if input image is really tall, but not very wide, otherwise it would crop white onto the sides
		factor = input_width / 1602;
		temp_height = 2000 * factor;
		new_size = [input_width, temp_height]
		}
	
	var cropBounds = computeCropBounds(cropDirection, input_width, input_height, new_size);
	require("photoshop").core.executeAsModal(() => cropDocument(cropBounds[0], cropBounds[1], cropBounds[2], cropBounds[3], 1602, 2000));
	await saveJPEG(projectfolder + "09_Fuer_Exporter/" + document.width + "x" + document.height + "/", document.name, 10);
	
	//////////////////////////////////////////////////////////////////// 2000*2000 ////////////////////////////////////////////////////////////////////
	await revertToSnapshot("start");
	var factor = input_width / 2000;
	var temp_height = 2000 * factor;
	var new_size = [input_width, temp_height];
	
	if (temp_height > input_height){ // if input image is really wide, but not very tall, otherwise it would crop white onto the top and bottom
		factor = input_height / 2000;
		temp_width = 2000 * factor;
		new_size = [temp_width, input_height];
		}
	
	var cropBounds = computeCropBounds(cropDirection, input_width, input_height, new_size);
	require("photoshop").core.executeAsModal(() => cropDocument(cropBounds[0], cropBounds[1], cropBounds[2], cropBounds[3], 2000, 2000));
	await saveJPEG(projectfolder + "09_Fuer_Exporter/" + document.width + "x" + document.height + "/", document.name, 10);
	//document.closeWithoutSaving();
})();
}

// #### start ####
document.getElementById("btnCropTL").addEventListener("click", () => handleClick("TL"));
document.getElementById("btnCropT").addEventListener("click", () => handleClick("T"));
document.getElementById("btnCropTR").addEventListener("click", () => handleClick("TR"));
document.getElementById("btnCropML").addEventListener("click", () => handleClick("ML"));
document.getElementById("btnCropM").addEventListener("click", () => handleClick("M"));
document.getElementById("btnCropMR").addEventListener("click", () => handleClick("MR"));
document.getElementById("btnCropBL").addEventListener("click", () => handleClick("BL"));
document.getElementById("btnCropB").addEventListener("click", () => handleClick("B"));
document.getElementById("btnCropBR").addEventListener("click", () => handleClick("BR"));

(Setting the api version to 2 makes the plugin unloadable, but I at least set it to Version 5)
Maybe its not a good idea to require batchPLay and lfs inside an asynchronous function?

OK, so I’ve rewritten your code into a more readable state and made some changes. I haven’t tested it, but it should at least be easier to debug now.
The main thing I did was refactor your main process that you were repeating 3 times into a single mainProcess function.
I also:

  • moved your imports to the top of the document and removed any unnecessary instances.

  • wrapped the whole lot in an executeAsModal to ensure that it catches anything it needs to

  • wrapped it all in a try/catch for error logging

I’ve left comments in the code as appropriate, hope it helps!

const { app, core } = require("photoshop").app;
const batchPlay = require("photoshop").action.batchPlay;
const lfs = require("uxp").storage.localFileSystem;

// #### helper functions ####
function createOutputFolders(projectfolder) {
  try {
    new Folder(projectfolder + "09_Fuer_Exporter/936x676/").create();
    new Folder(projectfolder + "09_Fuer_Exporter/1602x2000/").create();
    new Folder(projectfolder + "09_Fuer_Exporter/2000x2000/").create();
  } catch (e) {
    console.log("Error creating output folders", e)
  }

}

function computeCropBounds(cropDirection, input_width, input_height, new_size) {
  switch (cropDirection) {
    // Crop to the top left corner
    case "TL":
      var left = 0;
      var top = 0;
      var right = new_size[0];
      var bottom = new_size[1];
      break;

    // Crop to the top horizontally centered
    case "T":
      var left = (input_width - new_size[0]) / 2;
      var top = 0;
      var right = input_width - left;
      var bottom = new_size[1];
      break;

    // Crop to the top right corner
    case "TR":
      var left = input_width - new_size[0];
      var top = 0;
      var right = input_width;
      var bottom = new_size[1];
      break;

    // Crop to the left side, vertically centered
    case "ML":
      var left = 0;
      var top = (input_height - new_size[1]) / 2;
      var right = new_size[0];
      var bottom = input_height - top;
      break;

    // Crop to center
    case "M":
      var left = (input_width - new_size[0]) / 2;
      var top = (input_height - new_size[1]) / 2;
      var right = input_width - left;
      var bottom = input_height - top;
      break;

    // Crop to the right side, vertically centered
    case "MR":
      var left = input_width - new_size[0];
      var top = (input_height - new_size[1]) / 2;
      var right = input_width;
      var bottom = input_height - top;
      break;

    // Crop to the bottom left corner
    case "BL":
      var left = 0;
      var top = input_height - new_size[1];
      var right = new_size[0];
      var bottom = input_height;
      break;

    // Crop to the bottom - horizontally centered
    case "B":
      var left = (input_width - new_size[0]) / 2;
      var top = input_height - new_size[1];
      var right = input_width - left;
      var bottom = input_height;
      break;

    // Crop to the bottom right corner
    case "BR":
      var left = input_width - new_size[0];
      var top = input_height - new_size[1];
      var right = input_width;
      var bottom = input_height;
      break;
  }
  return [left, top, right, bottom];
}

// Why is this an async function if nothing inside is being awaited? Or should something actually be awaited...? you don't await cropDocument when you call it either. 
// I would imagine that being a crop action( which might take time) it should be awaited
async function cropDocument(
  cropL,
  cropT,
  cropR,
  cropB,
  output_width,
  output_height
) {
  app.activeDocument.crop(
    { left: cropL, top: cropT, right: cropR, bottom: cropB },
    0,
    output_width,
    output_height
  );
}

const makeSnapshot = async (name) => {
  return batchPlay(
    [
      {
        _obj: "make",
        _target: [
          {
            _ref: "snapshotClass",
          },
        ],
        from: {
          _ref: "historyState",
          _property: "currentHistoryState",
        },
        name: name,
        using: {
          _enum: "historyState",
          _value: "fullDocument",
        },
        _isCommand: true,
        _options: {},
      },
    ],
    {}
  );
};

const revertToSnapshot = async (name) => {
  return batchPlay(
    [
      {
        _obj: "select",
        _target: [
          {
            _ref: "snapshotClass",
            _name: name,
          },
        ],
        _isCommand: true,
        _options: {
          dialogOptions: "dontDisplay",
        },
      },
    ],
    {}
  );
};

const saveJPEG = async (saveFolder, newFilename, quality) => {
  const doc = app.activeDocument;
  const theNewFile = await saveFolder.createFile(newFilename, {
    overwrite: true,
  });
  const saveFile = await lfs.createSessionToken(theNewFile);
  return batchPlay(
    [
      {
        _obj: "save",
        as: {
          _obj: "JPEG",
          extendedQuality: quality,
          matteColor: {
            _enum: "matteColor",
            _value: "none",
          },
        },
        in: {
          _path: saveFile,
          _kind: "local",
        },
        documentID: doc.id,
        lowerCase: true,
        saveStage: {
          _enum: "saveStageType",
          _value: "saveBegin",
        },
        _isCommand: true,
        _options: {
          dialogOptions: "dontDisplay",
        },
      },
    ],
    {}
  );
};

// D.R.Y. principle - 'Don't Repeat Yourself' - you were repeating a whole chunk of code 3 times only changing a couple of variables, 
// so I've wrapped that up into a function which is called 3 times and passed the changing values each time.
const mainProcess = async (props) => {
  var document = app.activeDocument;
  var input_width = document.width;
  var input_height = document.height;

  var factor = input_width / props.valA;
  var temp_height = props.valB * factor;
  var new_size = [input_width, temp_height];

  if (temp_height > input_height) {
    // if input image is really wide, but not very tall, otherwise it would crop white onto the top and bottom
    factor = input_height / props.valB;
    temp_width = props.valA * factor;
    new_size = [temp_width, input_height];
  }

  var cropBounds = computeCropBounds(
    props.cropDirection,
    props.input_width,
    props.input_height,
    new_size
  );

  cropDocument(
    cropBounds[0],
    cropBounds[1],
    cropBounds[2],
    cropBounds[3],
    props.valA,
    props.valB
  );

  await saveJPEG(
    props.projectFolder +
      "09_Fuer_Exporter/" +
      document.width +
      "x" +
      document.height +
      "/",
    document.name,
    10
  );
};

// #### handle click events ####
const handleClick = async (cropDirection) => {
  // The whole code is now wrpped in an executeAsModal ensuring that all instances where it is required are caught
  return await core.executeAsModal(async () => {
      try {
      var document = app.activeDocument;
      var filepath = String(document.path);
      var projectfolder = filepath.substring(
        0,
        filepath.lastIndexOf("02_Capt_Export")
      );
  
      createOutputFolders(projectfolder);
  
      /** 936 x 676 */
      await makeSnapshot("start");
  
      // options for main process
      let options = {
        valA: 936,
        valB: 676,
        cropDirection,
        projectfolder,
      };
  
      // calling main process
      await mainProcess(options);
  
      /** 1602 x 2000 */
      await revertToSnapshot("start");
  
      options = {
        valA: 1602,
        valB: 2000,
        cropDirection,
        projectfolder,
      };
      await mainProcess(options);
  
      /** 2000 x 2000 */
      await revertToSnapshot("start");
  
      options = {
        valA: 2000,
        valB: 2000,
        cropDirection,
        projectfolder,
      };
  
      await mainProcess(options);
  
      //document.closeWithoutSaving();
    } catch (error) {
      console.error(error)
      console.trace(error)
    }
    });
};

So, its been a month, I am very sorry for the late reply. As a quick explanation: First I needed some time to go through your code, I didnt want to just say “I dont understand”. Then I had an epileptical seizure, was in a hospital for a few days and needed a bit to “reboot” my brain afterwards. Unfortunately I have a partial blackout of the weeks before that, the time where I started dabbling in UXP, so I had to relearn all I knew about that.

Thanks again for your effort. In the end I couldnt get your code to run at all, I fear its not Manifest 5, API 2, compliant? It’s throwing errors all over the place. I had fixed a few other things before, but ultimately failed because of the error “ReferenceError: CSEvent is not defined”. I couldnt find anything about that on the net.

In the end this thread, which I already linked above, rescued me: UXP Photoshop History State change
I think what I did wrong is that I didnt wrap it in executeAsModal back before I first asked? I think the executeAsModal is a pretty new addition and wasnt needed back when the original thread was opened? But don’t take my bet on this one.
Here is the working code for me:

core.executeAsModal(() => { 
    require("photoshop").action.batchPlay([{
        "_obj": "revert"
    }], {});
});

I really do not like the solution, as I do not like batchPlay (it’s terrible to read and I cant believe that Adobe hasnt properly implemented history states into UXP yet. Like, really, this is such a fundamental feature!) and also it doesnt actually solve the problem, but circumvents it (it reverts back to the beginning of the history, which works fine in my case, but next time I might want to set a history state freely on my own).

Yet again, thank you very much for your support! I find the UXP documentation to be hell, so your comment is very much appreciated!

My snapshot utility functions do work, I would posit that the errors are in your code or as a product of me refactoring it.
The code is also API 2/Manifest 5 compliant (although the only thing that applies to is executeAsModal and doesn’t have any relevance to the snapshot functions), and in the example above are wrapped in executeAsModal - the handleClick function wraps everything and has a pretty explicit comment explaining as such). executeAsModal isn’t new, it’s been part of UXP for nearly 3 years now I think.

I feel your frustration at the lack of a fully implemented API for UXP as yet, that said, batchplay ain’t all that bad when you get used to it, and if you wrap your batchplay in neat little utility functions you’re kinda just building your own API, easy enough to then combine them into a library for use in all projects.

As for you actual question - here’s a very basic demonstration of using my previous snapshot functions to create and revert to specific history states.

const makeSnapshot = async (name) => {
  return batchPlay(
    [
      {
        _obj: "make",
        _target: [
          {
            _ref: "snapshotClass",
          },
        ],
        from: {
          _ref: "historyState",
          _property: "currentHistoryState",
        },
        name: name,
        using: {
          _enum: "historyState",
          _value: "fullDocument",
        },
        _isCommand: true,
        _options: {},
      },
    ],
    {}
  );
};

const revertToSnapshot = async (name) => {
  return batchPlay(
    [
      {
        _obj: "select",
        _target: [
          {
            _ref: "snapshotClass",
            _name: name,
          },
        ],
        _isCommand: true,
        _options: {
          dialogOptions: "dontDisplay",
        },
      },
    ],
    {}
  );
};

const makeLayer = async () => {
  return await batchPlay(
    [
      {
        _obj: "make",
        _target: [
          {
            _ref: "layer",
          },
        ],
        _options: {
          dialogOptions: "dontDisplay",
        },
      },
    ],
    {}
  );
};

document.querySelector("#testButton").addEventListener("click", async () => {
  const document = app.activeDocument;

  console.log("Button clicked 👆");

  await core.executeAsModal(async () => {
    console.log("Layer count: ", document.layers.length);

    console.log("🛠️ Making first layer");
    await makeLayer();
    console.log("Layer count is: ", document.layers.length);
    console.log("📸 Taking first snapshot");
    await makeSnapshot("first");

    console.log("🛠️ Making second layer");
    await makeLayer();
    console.log("Layer count is: ", document.layers.length);
    console.log("📸 Taking second snapshot");
    await makeSnapshot("second");

    console.log("📷 Reverting to first snapshot");
    await revertToSnapshot("first");
    console.log("Layer count is: ", document.layers.length);
  });
});

Default snapshot

First snapshot

Second snapshot

1 Like

I am not sure where you found it but I can say it does not work because it does not exist (today 27.7.2023)

These AI are lying to developers quite often. The model is not updated that often. And even if it does have all the relevant information it hallucinates quite often.

You could use something like app.activeDocument.activeHistoryState = app.activeDocument.historyStates[0]

Anyway, snapshots are better than history steps. Because users might set it to preserve e.g. only 3 history steps or any other number and then you have nothing to return back. Meanwhile, snapshots are persistent as long as your document is opened.

Alternatively, you could use commit:false in history state suspension. https://developer.adobe.com/photoshop/uxp/2022/ps_reference/media/executeasmodal/#history-state-suspension So anything you do during that modal execution will be performed but reverted back once modal execution ends.

3 Likes

Nice! I didn’t know that, what a great feature :muscle:

Timothy, you are the man! Sorry for taking up so much of your time! I was able to adapt this now. I personally dislike the way you declare functions, are there any obvious benefits to write those as arrow functions?

const { app, core } = require("photoshop");

async function makeSnapshot(name) {
  return require("photoshop").action.batchPlay(
    [
      {
        _obj: "make",
        _target: [
          {
            _ref: "snapshotClass",
          },
        ],
        from: {
          _ref: "historyState",
          _property: "currentHistoryState",
        },
        name: name,
        using: {
          _enum: "historyState",
          _value: "fullDocument",
        },
        _isCommand: true,
        _options: {},
      },
    ],
    {}
  );
};

async function revertToSnapshot(name) {
  return require("photoshop").action.batchPlay(
    [
      {
        _obj: "select",
        _target: [
          {
            _ref: "snapshotClass",
            _name: name,
          },
        ],
        _isCommand: true,
        _options: {
          dialogOptions: "dontDisplay",
        },
      },
    ],
    {}
  );
};


async function main(){
    const doc = app.activeDocument;

    await core.executeAsModal(() => {
        makeSnapshot("first");
        doc.crop({left: 100, top: 100, right: 936, bottom: 676}, 0, 936, 676);       
        revertToSnapshot("first");
    });
}

document.getElementById('btnCreateFolders').addEventListener('click', () => { main(); });

Thanks, Jarda, too, for stepping in. I saw that “commit:false” thing, but being the noob I am, I didnt understand it properly, plus I have some things inside the modal scope that I would like to be persistent and not be recomputed every time. Also, yeah, LLMs are just stochastics, I am sorry I didnt convey that I didnt believe what I read there, hence wanted to ask a real person for clarification on the subject.

Functions can be a matter of personal preference for sure!
Function declarations and function expressions behave differently and whilst a lot of the time they’re interchangeable there are specific cases where one trumps the other - class constructors (no arrow syntax here), this scope (totally different for each), function scope (is it hoisted and globally scoped or not).

In the instances where it comes down to personal choice, I like arrow functions for a number of reasons:

=> More concise thanks to implicit return

Consider this:

function multiply(x, y) {
 return x * y
}

versus this:

const multiply = (x, y) => x * y 

=> More readable

I guess this is personal preference, but I find the ES6 and arrow syntax much more readable, especially when using verbose function and variable names.
I think anonymous callbacks are also a great example of arrow syntax making everyone’s life easier(especially in the case of long chains of array methods):

[1,2,3].map(function(value) { 
  return `Value is ${value}`
})

[1,2,3].map(value => `Value is ${value}`)

=> Currying

Related to the above, arrow functions make currying much more concise and readable. Here’s a comparison of different approaches to calculating pixel distances:

const pixels = [
  {
    name: 'Pixel 1',
    position: { x: 3, y: 5}
  },
  {
    name: 'Pixel 2',
    position: { x: -4, y: -4}
  },
  {
    name: 'Pixel 3',
    position: { x: -2, y: 8}
  }
];

const comparisonPixel = { x:0, y:0};


/** ES5 No currying 
* 7 lines of code
*/
function distanceNoCurrying(start, end) {
  return Math.sqrt( Math.pow(end.x-start.x, 2) + Math.pow(end.y-start.y, 2))
};

let distancesNoCurry = [];

pixels.forEach(pixel => {
    distancesNoCurry.push(distanceNoCurrying(pixel.position, comparisonPixel))
})


/** ES5 Currying
* 9 lines of code
*/
var ES5Curry = function(start) {
  return function(end) {
    return Math.sqrt( Math.pow(end.x-start.x, 2) + Math.pow(end.y-start.y, 2) )
  };
};

let ES5Currying = [];

pixels.forEach(pixel => {
    ES5Currying.push(ES5Curry(pixel.position)(comparisonPixel))
})


/** ES6 Currying 
* 3 lines of code
*/
const ES6Currying = (start) => (end) => Math.sqrt( Math.pow(end.x-start.x, 2) + Math.pow(end.y-start.y, 2) );

const distanceFromComparisonPixel = ES6Currying(comparisonPixel);

const curriedDistances = pixels.map(pixel => pixel.position).map(distanceFromComparisonPixel)

Can snapshots be overwritten? It seems for me they are not, and I am not sure how to delete them without undo the steps.

Snapshots can be deleted, in PS you can right-click them to bring up a contextual menu.

Yes, but when you delete one, it undoes those steps as well. So if I use a Snapshot in my uxp and:
-save snapshot
-do some stuff
-revert to saved snapshot
-delete snapshot
it basically puts me back as though I did not revert to the saved snapshot

You can delete a snapshot without selecting(activating) it.

I tried that via batchplay but it still seems to select it, or at least it undoes the steps.

async function deleteSnapshot(execContext, name) {
  return require("photoshop").action.batchPlay(
    [
      {
        _obj: "delete",
        _target: [
          {
            _ref: "snapshotClass",
            _name: name,
          },
        ],
        _isCommand: true,
        _options: {
          dialogOptions: "dontDisplay",
        },
      },
    ],
    {}
  );
};

What is the active history item when you do so?

I am guessing it would be the snapshot since I just reverted to it.
Are you saying I need to add some other actions after reverting to the snapshot and THEN delete the snapshot? I had tried that by just toggling a layer visibility but that did not seem to work (although I am not sure why it wouldn’t)

Sorry for the late reply, I am now in the finishing stages of my little plugin experiment.
Now that everything else works, I recognized I havent fully understood the snapshots? (Never used them in my normal day to day use.)

The thing is, if I make a snapshot, the last command in the history gets undone.

async function initImage() {
    await core.executeAsModal(() => { makeBGLayer(); });
    await core.executeAsModal(() => { initialCrop(); });
    await core.executeAsModal(() => { makeSnapshot("init"); });
}
async function makeSnapshot(name) {
  return require("photoshop").action.batchPlay(
    [
      {
        _obj: "make",
        _target: [
          {
            _ref: "snapshotClass",
          },
        ],
        from: {
          _ref: "historyState",
          _property: "currentHistoryState",
        },
        name: name,
        using: {
          _enum: "historyState",
          _value: "fullDocument",
        },
        _isCommand: true,
        _options: {},
      },
    ],
    {}
  );
};

The cropping works fine if I do not set the snapshot. But if I set one, the snapshot does not contain the crop I just performed.
In the crop function I first select all white pixels, then invert the selection and then crop. The selection is present in the snapshot, but the crop is not.

I already tried to circumvent this with a blank batchplay function to generate a step I do not care about.

async function emptyUndo(){
    await require("photoshop").action.batchPlay([
            {
                "_obj": "undo"
            }
        ],
        {}
    );
}

I also tried to first delete the history, then make the snapshot.

async function clearHistory(){
    const result = await batchPlay(
       [
          {
             _obj: "clearEvent",
             _target: [
                {
                   _ref: "property",
                   _property: "historyStates"
                },
                {
                   _ref: "document",
                   _enum: "ordinal",
                   _value: "targetEnum"
                }
             ],
             _options: {
                dialogOptions: "dontDisplay"
             }
          }
       ],
       {}
    );
}

Soo… uhm… sorry for being dumb! What did I not understand this time? :slight_smile:
Thank you in advance!

I was never able to get snapshots to work the way they implied it should. I always had issues with it. Instead, I ended up making a step with a history state that does not apply. So it basically runs everything in that step but then undos it.
await hostControl.resumeHistory(suspensionID, false);

1 Like

Agree with @JasonM, the easiest (and in my humble opinion full proof) way of creating a snapshot that works very well is to use the UXP’s undo/redo, via hostControl (as part of the executionContext, that gets passed along by executeAsModal). Check out ‘History State Suspension’ in the docs.

The basic idea is to suspend history right before doing all the snapshot business (i.e. all the stuff that you need to do to your doc but want to discard later) and then afterwards roll back (via. resumeHistory with commit set to false)