@bubblydoo/uxp-toolkit 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.turbo/turbo-build.log +15 -0
  2. package/CHANGELOG.md +7 -0
  3. package/dist/index.d.ts +271 -0
  4. package/dist/index.js +733 -0
  5. package/package.json +41 -0
  6. package/src/commands-library/getLayerProperties.ts +32 -0
  7. package/src/commands-library/renameLayer.ts +15 -0
  8. package/src/commands-library/renameLayer.uxp-test.ts +32 -0
  9. package/src/core/batchPlay.ts +16 -0
  10. package/src/core/command.ts +130 -0
  11. package/src/core/executeAsModal.ts +101 -0
  12. package/src/core/suspendHistory.ts +15 -0
  13. package/src/core/suspendHistory.uxp-test.ts +18 -0
  14. package/src/core-wrappers/executeAsModalAndSuspendHistory.ts +11 -0
  15. package/src/dom/getFlattenedDomLayersList.ts +43 -0
  16. package/src/dom/photoshopDomLayersToTree.ts +18 -0
  17. package/src/error-sourcemaps/sourcemaps.ts +100 -0
  18. package/src/error-sourcemaps/sourcemaps.uxp-test.ts +24 -0
  19. package/src/errors/ut-error.ts +6 -0
  20. package/src/filesystem/openFileByPath.ts +10 -0
  21. package/src/general-tree/flattenTree.ts +12 -0
  22. package/src/general-tree/layerRef.ts +4 -0
  23. package/src/general-tree/mapTree.ts +11 -0
  24. package/src/general-tree/mapTreeRef.ts +11 -0
  25. package/src/general-tree/treeTypes.ts +7 -0
  26. package/src/index.ts +73 -0
  27. package/src/metadata-storage/metadataStorage.ts +66 -0
  28. package/src/metadata-storage/metadataStorage.uxp-test.ts +35 -0
  29. package/src/node-compat/path/resolvePath.ts +19 -0
  30. package/src/other/applicationInfo.ts +169 -0
  31. package/src/other/applicationInfo.uxp-test.ts +11 -0
  32. package/src/other/clipboard.ts +10 -0
  33. package/src/other/clipboard.uxp-test.ts +17 -0
  34. package/src/other/uxpEntrypoints.ts +9 -0
  35. package/src/ut-tree/getFlattenedLayerDescriptorsList.ts +72 -0
  36. package/src/ut-tree/getLayerEffects.ts +41 -0
  37. package/src/ut-tree/getLayerProperties.ts +35 -0
  38. package/src/ut-tree/photoshopLayerDescriptorsToUTLayers.ts +182 -0
  39. package/src/ut-tree/photoshopLayerDescriptorsToUTLayers.uxp-test.ts +52 -0
  40. package/src/ut-tree/psLayerRef.ts +4 -0
  41. package/src/ut-tree/utLayersToTree.ts +21 -0
  42. package/src/util/utLayerToLayer.ts +41 -0
  43. package/test/fixtures/clipping-layers.psd +0 -0
  44. package/test/fixtures/one-layer.psd +0 -0
  45. package/test/index.ts +21 -0
  46. package/test/meta-tests/executeAsModal.uxp-test.ts +38 -0
  47. package/test/meta-tests/suspendHistory.uxp-test.ts +27 -0
  48. package/tsconfig.json +13 -0
  49. package/tsup.config.ts +9 -0
  50. package/uxp-tests.json +13 -0
@@ -0,0 +1,182 @@
1
+ import { type LayerDescriptor } from "./getFlattenedLayerDescriptorsList";
2
+ import { executeAsModal } from "../core/executeAsModal";
3
+ import { createGetLayerCommand } from "./getLayerEffects";
4
+
5
+ type UTLayerKind = "pixel" | "adjustment-layer" | "text" | "curves" | "smartObject" | "video" | "group" | "threeD" | "gradientFill" | "pattern" | "solidColor" | "background";
6
+
7
+ type UTBlendMode = "normal" | "dissolve" | "darken" | "multiply" | "colorBurn" | "linearBurn" | "darkerColor" | "lighten" | "screen" | "colorDodge" | "linearDodge" | "lighterColor" | "overlay" | "softLight" | "hardLight" | "vividLight" | "linearLight" | "pinLight" | "hardMix" | "difference" | "exclusion" | "blendSubtraction" | "blendDivide" | "hue" | "saturation" | "color" | "luminosity" | "passThrough";
8
+
9
+ type UTLayerBuilder = {
10
+ name: string;
11
+ docId: number;
12
+ id: number;
13
+ visible: boolean;
14
+ kind: UTLayerKind;
15
+ blendMode: UTBlendMode;
16
+ effects: Record<string, boolean>;
17
+ isClippingMask: boolean;
18
+ layers?: UTLayerBuilder[];
19
+ };
20
+
21
+ export type UTLayer = Readonly<Omit<UTLayerBuilder, "layers">> & {
22
+ layers?: UTLayer[];
23
+ };
24
+
25
+ const layerKindMap = new Map<number, UTLayerKind>([
26
+ [1, "pixel"],
27
+ [2, "adjustment-layer"], // All adjustment layers
28
+ [3, "text"],
29
+ [4, "curves"],
30
+ [5, "smartObject"],
31
+ [6, "video"],
32
+ [7, "group"],
33
+ [8, "threeD"],
34
+ [9, "gradientFill"],
35
+ [10, "pattern"],
36
+ [11, "solidColor"],
37
+ [12, "background"], // according to the internet but the actual value is undefined
38
+ ]);
39
+
40
+ const blendModes: string[] = [
41
+ "normal",
42
+ "dissolve",
43
+ "darken",
44
+ "multiply",
45
+ "colorBurn",
46
+ "linearBurn",
47
+ "darkerColor",
48
+ "lighten",
49
+ "screen",
50
+ "colorDodge",
51
+ "linearDodge",
52
+ "lighterColor",
53
+ "overlay",
54
+ "softLight",
55
+ "hardLight",
56
+ "vividLight",
57
+ "linearLight",
58
+ "pinLight",
59
+ "hardMix",
60
+ "difference",
61
+ "exclusion",
62
+ "blendSubtraction",
63
+ "blendDivide",
64
+ "hue",
65
+ "saturation",
66
+ "color",
67
+ "luminosity",
68
+ "passThrough",
69
+ ] satisfies UTBlendMode[];
70
+
71
+ const getLayerSectionValue = (layer: LayerDescriptor): string | undefined => {
72
+ if (typeof layer.layerSection === "string") {
73
+ return layer.layerSection;
74
+ }
75
+ if (
76
+ layer.layerSection &&
77
+ typeof layer.layerSection === "object" &&
78
+ "_value" in layer.layerSection
79
+ ) {
80
+ return layer.layerSection._value;
81
+ }
82
+ return undefined;
83
+ };
84
+
85
+ const getLayerKind = (layer: LayerDescriptor): UTLayerKind => {
86
+ const kind = layerKindMap.get(layer.layerKind);
87
+ if (!kind) {
88
+ throw new Error(`Unknown layer kind: ${layer.layerKind}`);
89
+ }
90
+ return kind;
91
+ };
92
+
93
+ const getBlendMode = (layer: LayerDescriptor): UTBlendMode => {
94
+ const mode = layer.mode._value;
95
+ if (!blendModes.includes(mode)) {
96
+ throw new Error(`Unknown blend mode: ${mode}`);
97
+ }
98
+ return mode as UTBlendMode;
99
+ };
100
+
101
+ const determineLayerSection = (layer: LayerDescriptor): string => {
102
+ const section = getLayerSectionValue(layer);
103
+ const isGroupEnd =
104
+ layer.name === "</Layer group>" ||
105
+ layer.name === "</Layer set>" ||
106
+ section === "layerSectionEnd";
107
+
108
+ const isGroupStart = section === "layerSectionStart";
109
+ return isGroupStart ? "start" : isGroupEnd ? "end" : "normal";
110
+ };
111
+
112
+ // Generate a tree from a flat list of layer descriptors
113
+ export const photoshopLayerDescriptorsToUTLayers = async (layers: LayerDescriptor[]): Promise<UTLayer[]> => {
114
+ const root: UTLayerBuilder[] = [];
115
+ const stack: {
116
+ layers: UTLayerBuilder[];
117
+ }[] = [{ layers: root }];
118
+
119
+ // 1. Prepare a single batch request for all layers
120
+ const commands = layers.map((layer) => createGetLayerCommand({ docId: layer.docId, id: layer.layerID }));
121
+
122
+ // 2. Execute one batch command instead of N commands
123
+ const batchResults = await executeAsModal("Get Layer Effects Data", async (ctx) => {
124
+ return await ctx.batchPlayCommands(commands);
125
+ });
126
+
127
+ // 3. Create a fast lookup map for the results
128
+ const effectsMap = new Map<number, UTLayerBuilder["effects"]>();
129
+ batchResults.forEach((result, index) => {
130
+ const layerId = layers[index]!.layerID;
131
+ const data = result.layerEffects;
132
+ const effects: UTLayerBuilder["effects"] = {};
133
+ if (data) {
134
+ for (const effect in data) {
135
+ effects[effect] = Array.isArray(data[effect]) ? data[effect].some((e) => e.enabled) : !!data[effect]?.enabled;
136
+ }
137
+ }
138
+ effectsMap.set(layerId, effects);
139
+ });
140
+
141
+ for (const layer of layers) {
142
+ // Determine if the layer is a group start or end
143
+ const sectionType = determineLayerSection(layer);
144
+
145
+ const isClippingMask = !!batchResults.find((res, index) => {
146
+ return layer.layerID === res.layerID;
147
+ })?.group;
148
+
149
+ // Handle group end
150
+ if (sectionType === "end") {
151
+ if (stack.length > 1) {
152
+ stack.pop();
153
+ }
154
+ continue;
155
+ }
156
+ // Create the node
157
+ const node: UTLayerBuilder = {
158
+ name: layer.name,
159
+ docId: layer.docId,
160
+ id: layer.layerID,
161
+ visible: layer.visible,
162
+ kind: getLayerKind(layer),
163
+ blendMode: getBlendMode(layer),
164
+ isClippingMask,
165
+ effects: effectsMap.get(layer.layerID) || {},
166
+ };
167
+
168
+ // Add the node to the current level
169
+ const current = stack[stack.length - 1];
170
+ current!.layers.push(node);
171
+
172
+ // Handle group start
173
+ if (sectionType === "start") {
174
+ node.layers = [];
175
+ // Push children array to stack to process content
176
+ stack.push({ layers: node.layers });
177
+ }
178
+ }
179
+
180
+ // Cast to the readonly Tree type
181
+ return root as UTLayer[];
182
+ };
@@ -0,0 +1,52 @@
1
+ import type { Test } from "@bubblydoo/uxp-test-framework";
2
+ import { photoshopLayerDescriptorsToUTLayers } from "./photoshopLayerDescriptorsToUTLayers";
3
+ import { openFileByPath } from "../filesystem/openFileByPath";
4
+ import { getFlattenedLayerDescriptorsList } from "./getFlattenedLayerDescriptorsList";
5
+ import { expect } from "chai";
6
+
7
+ export const photoshopLayerDescriptorsToUTLayersTest: Test = {
8
+ name: "photoshopLayerDescriptorsToUTLayers",
9
+ description: "Test the photoshopLayerDescriptorsToUTLayers function",
10
+ run: async () => {
11
+ const doc = await openFileByPath("plugin:/fixtures/clipping-layers.psd");
12
+ const descriptors = await getFlattenedLayerDescriptorsList(doc.id);
13
+ const layers = await photoshopLayerDescriptorsToUTLayers(descriptors);
14
+ expect(layers).to.containSubset([
15
+ {
16
+ name: "circle",
17
+ visible: true,
18
+ kind: "pixel",
19
+ blendMode: "normal",
20
+ isClippingMask: true,
21
+ effects: {},
22
+ },
23
+ {
24
+ name: "group",
25
+ visible: true,
26
+ kind: "group",
27
+ blendMode: "passThrough",
28
+ isClippingMask: false,
29
+ effects: {},
30
+ layers: [
31
+ {
32
+ name: "green square",
33
+ visible: true,
34
+ kind: "pixel",
35
+ blendMode: "normal",
36
+ isClippingMask: true,
37
+ effects: {},
38
+ },
39
+ {
40
+ name: "red square",
41
+ id: 2,
42
+ visible: true,
43
+ kind: "pixel",
44
+ blendMode: "normal",
45
+ isClippingMask: false,
46
+ effects: {},
47
+ },
48
+ ],
49
+ },
50
+ ]);
51
+ },
52
+ };
@@ -0,0 +1,4 @@
1
+ export type PsLayerRef = {
2
+ id: number;
3
+ docId: number;
4
+ };
@@ -0,0 +1,21 @@
1
+ import type { Tree } from "../general-tree/treeTypes";
2
+ import type { UTLayer } from "./photoshopLayerDescriptorsToUTLayers";
3
+
4
+ export type UTLayerWithoutChildren = Omit<UTLayer, "layers">;
5
+
6
+ export function utLayersToTree(layer: UTLayer[]): Tree<UTLayerWithoutChildren> {
7
+ return layer.map((layer) => ({
8
+ ref: {
9
+ name: layer.name,
10
+ docId: layer.docId,
11
+ id: layer.id,
12
+ visible: layer.visible,
13
+ kind: layer.kind,
14
+ blendMode: layer.blendMode,
15
+ isClippingMask: layer.isClippingMask,
16
+ effects: layer.effects,
17
+ },
18
+ name: layer.name,
19
+ children: layer.layers ? utLayersToTree(layer.layers) : undefined,
20
+ }));
21
+ }
@@ -0,0 +1,41 @@
1
+ import type { Layer as DomLayer } from "photoshop/dom/Layer";
2
+ import type { UTLayer } from "../ut-tree/photoshopLayerDescriptorsToUTLayers";
3
+ import { app } from "photoshop";
4
+ import { getFlattenedDomLayersList } from "../dom/getFlattenedDomLayersList";
5
+
6
+ export function utLayerToDomLayer(layer: UTLayer): DomLayer {
7
+ const doc = app.documents.find((d) => d.id === layer.docId);
8
+ if (!doc) {
9
+ throw new Error(`Document with id ${layer.docId} not found.`);
10
+ }
11
+ const allLayers = getFlattenedDomLayersList(doc.layers);
12
+ const domLayer = allLayers.find((l) => l.id === layer.id);
13
+ if (!domLayer) {
14
+ throw new Error(
15
+ `Layer with id ${layer.id} not found in document ${layer.docId}.`
16
+ );
17
+ }
18
+ return domLayer;
19
+ }
20
+
21
+ export function utLayersToDomLayers(layers: UTLayer[]): DomLayer[] {
22
+ if (layers.length === 0) return [];
23
+ const docId = layers[0]!.docId;
24
+ if (!layers.every((l) => l.docId === docId)) {
25
+ throw new Error("All layers must be from the same document.");
26
+ }
27
+ const doc = app.documents.find((d) => d.id === docId);
28
+ if (!doc) {
29
+ throw new Error(`Document with id ${docId} not found.`);
30
+ }
31
+ const allLayers = getFlattenedDomLayersList(doc.layers);
32
+ return layers.map((l) => {
33
+ const domLayer = allLayers.find((dl) => dl.id === l.id);
34
+ if (!domLayer) {
35
+ throw new Error(
36
+ `Layer with id ${l.id} not found in document ${docId}.`
37
+ );
38
+ }
39
+ return domLayer;
40
+ });
41
+ }
Binary file
package/test/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { applicationInfoTest } from "../src/other/applicationInfo.uxp-test";
2
+ import { renameLayerTest } from "../src/commands-library/renameLayer.uxp-test";
3
+ import { suspendHistoryErrorTest } from "./meta-tests/suspendHistory.uxp-test";
4
+ import { executeAsModalErrorTest } from "./meta-tests/executeAsModal.uxp-test";
5
+ import { suspendHistoryTest } from "../src/core/suspendHistory.uxp-test";
6
+ import { sourcemapsTest } from "../src/error-sourcemaps/sourcemaps.uxp-test";
7
+ import { clipboardTest } from "../src/other/clipboard.uxp-test";
8
+ import { photoshopLayerDescriptorsToUTLayersTest } from "../src/ut-tree/photoshopLayerDescriptorsToUTLayers.uxp-test";
9
+ import { metadataStorageTest } from "../src/metadata-storage/metadataStorage.uxp-test";
10
+
11
+ export const tests = [
12
+ applicationInfoTest,
13
+ renameLayerTest,
14
+ suspendHistoryErrorTest,
15
+ executeAsModalErrorTest,
16
+ suspendHistoryTest,
17
+ sourcemapsTest,
18
+ clipboardTest,
19
+ photoshopLayerDescriptorsToUTLayersTest,
20
+ metadataStorageTest,
21
+ ];
@@ -0,0 +1,38 @@
1
+ import { core } from "photoshop";
2
+ import type { Test } from "@bubblydoo/uxp-test-framework";
3
+ import { expect } from "chai";
4
+
5
+ export const executeAsModalErrorTest: Test = {
6
+ name: "meta: executeAsModal should throw correctly",
7
+ async run() {
8
+ let threw = false;
9
+ try {
10
+ await core.executeAsModal(
11
+ async () => {
12
+ throw new Error("Uncaught error");
13
+ },
14
+ {
15
+ commandName: "Test",
16
+ }
17
+ );
18
+ } catch (e) {
19
+ threw = true;
20
+ }
21
+ expect(threw).to.be.true;
22
+ },
23
+ };
24
+
25
+ export const executeAsModalReturnTest: Test = {
26
+ name: "meta: executeAsModal should return correctly",
27
+ async run() {
28
+ const result = await core.executeAsModal(
29
+ async () => {
30
+ return 'test'
31
+ },
32
+ {
33
+ commandName: "Test",
34
+ }
35
+ );
36
+ expect(result).to.equal('test');
37
+ },
38
+ };
@@ -0,0 +1,27 @@
1
+ import { app } from "photoshop";
2
+ import type { Test } from "@bubblydoo/uxp-test-framework";
3
+ import { expect } from "chai";
4
+
5
+ export const suspendHistoryErrorTest: Test = {
6
+ name: "meta: suspendHistory should throw correctly",
7
+ async run() {
8
+ const document = app.activeDocument;
9
+ if (!document) {
10
+ throw new Error("No active document");
11
+ }
12
+
13
+ let threw = false;
14
+ try {
15
+ await document.suspendHistory(
16
+ async (context) => {
17
+ throw new Error("Uncaught error");
18
+ },
19
+ "Test"
20
+ );
21
+ } catch (e) {
22
+ threw = true;
23
+ }
24
+ expect(threw).to.be.true;
25
+ },
26
+ };
27
+
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "@bubblydoo/tsconfig/tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "allowJs": true,
5
+ "moduleDetection": "force",
6
+ "verbatimModuleSyntax": true,
7
+ "noUncheckedIndexedAccess": true,
8
+ "noImplicitOverride": true,
9
+ "rootDir": ".",
10
+ "typeRoots": ["./node_modules/@adobe-uxp-types", "./node_modules/@types"]
11
+ },
12
+ "include": ["src", "test"]
13
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: "esm",
6
+ dts: true,
7
+ external: ["photoshop", "uxp"],
8
+ outDir: "dist",
9
+ });
package/uxp-tests.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "./node_modules/@bubblydoo/uxp-test-framework/uxp-tests-json-schema.json",
3
+ "outDir": "uxp-tests-plugin",
4
+ "testsFile": "test/index.ts",
5
+ "testFixturesDir": "test/fixtures",
6
+ "plugin": {
7
+ "id": "co.bubblydoo.uxp-toolkit-test-plugin",
8
+ "name": "UXP Toolkit Tests"
9
+ },
10
+ "vite": {
11
+ "enableTsconfigPathsPlugin": true
12
+ }
13
+ }