@bubblydoo/uxp-toolkit 0.0.4 → 0.0.6

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.
@@ -1,6 +1,4 @@
1
- import { type LayerDescriptor } from "./getFlattenedLayerDescriptorsList";
2
- import { executeAsModal } from "../core/executeAsModal";
3
- import { createGetLayerCommand } from "./getLayerEffects";
1
+ import type { LayerDescriptor } from "./getDocumentLayerDescriptors";
4
2
 
5
3
  type UTLayerKind = "pixel" | "adjustment-layer" | "text" | "curves" | "smartObject" | "video" | "group" | "threeD" | "gradientFill" | "pattern" | "solidColor" | "background";
6
4
 
@@ -15,6 +13,7 @@ type UTLayerBuilder = {
15
13
  blendMode: UTBlendMode;
16
14
  effects: Record<string, boolean>;
17
15
  isClippingMask: boolean;
16
+ background: boolean;
18
17
  layers?: UTLayerBuilder[];
19
18
  };
20
19
 
@@ -22,6 +21,8 @@ export type UTLayer = Readonly<Omit<UTLayerBuilder, "layers">> & {
22
21
  layers?: UTLayer[];
23
22
  };
24
23
 
24
+ export type UTLayerMultiGetOnly = Omit<UTLayer, "effects">;
25
+
25
26
  const layerKindMap = new Map<number, UTLayerKind>([
26
27
  [1, "pixel"],
27
28
  [2, "adjustment-layer"], // All adjustment layers
@@ -68,20 +69,6 @@ const blendModes: string[] = [
68
69
  "passThrough",
69
70
  ] satisfies UTBlendMode[];
70
71
 
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
72
  const getLayerKind = (layer: LayerDescriptor): UTLayerKind => {
86
73
  const kind = layerKindMap.get(layer.layerKind);
87
74
  if (!kind) {
@@ -98,54 +85,16 @@ const getBlendMode = (layer: LayerDescriptor): UTBlendMode => {
98
85
  return mode as UTBlendMode;
99
86
  };
100
87
 
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[]> => {
88
+ export function photoshopLayerDescriptorsToUTLayers(layers: LayerDescriptor[]): UTLayer[] {
114
89
  const root: UTLayerBuilder[] = [];
115
90
  const stack: {
116
91
  layers: UTLayerBuilder[];
117
92
  }[] = [{ layers: root }];
118
93
 
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
94
  for (const layer of layers) {
142
95
  // Determine if the layer is a group start or end
143
96
  const sectionType = determineLayerSection(layer);
144
97
 
145
- const isClippingMask = !!batchResults.find((res, index) => {
146
- return layer.layerID === res.layerID;
147
- })?.group;
148
-
149
98
  // Handle group end
150
99
  if (sectionType === "end") {
151
100
  if (stack.length > 1) {
@@ -153,6 +102,7 @@ export const photoshopLayerDescriptorsToUTLayers = async (layers: LayerDescripto
153
102
  }
154
103
  continue;
155
104
  }
105
+
156
106
  // Create the node
157
107
  const node: UTLayerBuilder = {
158
108
  name: layer.name,
@@ -161,8 +111,9 @@ export const photoshopLayerDescriptorsToUTLayers = async (layers: LayerDescripto
161
111
  visible: layer.visible,
162
112
  kind: getLayerKind(layer),
163
113
  blendMode: getBlendMode(layer),
164
- isClippingMask,
165
- effects: effectsMap.get(layer.layerID) || {},
114
+ isClippingMask: layer.group,
115
+ effects: getEffects(layer),
116
+ background: layer.background,
166
117
  };
167
118
 
168
119
  // Add the node to the current level
@@ -180,3 +131,24 @@ export const photoshopLayerDescriptorsToUTLayers = async (layers: LayerDescripto
180
131
  // Cast to the readonly Tree type
181
132
  return root as UTLayer[];
182
133
  };
134
+
135
+ const determineLayerSection = (layer: LayerDescriptor): "start" | "end" | "normal" => {
136
+ const section = layer.layerSection._value;
137
+ const isGroupEnd =
138
+ layer.name === "</Layer group>" ||
139
+ layer.name === "</Layer set>" ||
140
+ section === "layerSectionEnd";
141
+
142
+ const isGroupStart = section === "layerSectionStart";
143
+ return isGroupStart ? "start" : isGroupEnd ? "end" : "normal";
144
+ };
145
+
146
+ function getEffects(layer: LayerDescriptor): Record<string, boolean> {
147
+ const effects: Record<string, boolean> = {};
148
+ if (layer.layerEffects) {
149
+ for (const effect in layer.layerEffects) {
150
+ effects[effect] = Array.isArray(layer.layerEffects[effect]) ? layer.layerEffects[effect].some((e) => e.enabled) : !!layer.layerEffects[effect]?.enabled;
151
+ }
152
+ }
153
+ return effects;
154
+ }
@@ -1,16 +1,19 @@
1
1
  import type { Test } from "@bubblydoo/uxp-test-framework";
2
2
  import { photoshopLayerDescriptorsToUTLayers } from "./photoshopLayerDescriptorsToUTLayers";
3
3
  import { openFileByPath } from "../filesystem/openFileByPath";
4
- import { getFlattenedLayerDescriptorsList } from "./getFlattenedLayerDescriptorsList";
5
4
  import { expect } from "chai";
5
+ import { getDocumentLayerDescriptors } from "./getDocumentLayerDescriptors";
6
6
 
7
7
  export const photoshopLayerDescriptorsToUTLayersTest: Test = {
8
8
  name: "photoshopLayerDescriptorsToUTLayers",
9
9
  description: "Test the photoshopLayerDescriptorsToUTLayers function",
10
10
  run: async () => {
11
11
  const doc = await openFileByPath("plugin:/fixtures/clipping-layers.psd");
12
- const descriptors = await getFlattenedLayerDescriptorsList(doc.id);
13
- const layers = await photoshopLayerDescriptorsToUTLayers(descriptors);
12
+ const descriptors = await getDocumentLayerDescriptors(doc.id);
13
+
14
+ console.log(descriptors);
15
+
16
+ const layers = photoshopLayerDescriptorsToUTLayers(descriptors);
14
17
  expect(layers).to.containSubset([
15
18
  {
16
19
  name: "circle",
@@ -50,3 +53,33 @@ export const photoshopLayerDescriptorsToUTLayersTest: Test = {
50
53
  ]);
51
54
  },
52
55
  };
56
+
57
+ export const photoshopLayerDescriptorsToUTLayersTest2: Test = {
58
+ name: "photoshopLayerDescriptorsToUTLayers",
59
+ description: "Test the photoshopLayerDescriptorsToUTLayers function",
60
+ run: async () => {
61
+ const doc = await openFileByPath("plugin:/fixtures/one-layer-with-bg.psd");
62
+ const descriptors = await getDocumentLayerDescriptors(doc.id);
63
+ console.log(descriptors);
64
+ const layers = photoshopLayerDescriptorsToUTLayers(descriptors);
65
+ console.log(layers);
66
+ expect(layers).to.containSubset([
67
+ {
68
+ name: "Layer 1",
69
+ visible: true,
70
+ kind: "pixel",
71
+ blendMode: "normal",
72
+ isClippingMask: false,
73
+ effects: {},
74
+ },
75
+ {
76
+ name: "Background",
77
+ visible: true,
78
+ kind: "background",
79
+ blendMode: "normal",
80
+ isClippingMask: false,
81
+ effects: {},
82
+ },
83
+ ]);
84
+ },
85
+ };
@@ -0,0 +1,94 @@
1
+ import { expect, it } from "vitest";
2
+ import { utLayersToText } from "./utLayersToText";
3
+
4
+ it("converts a single layer to text", () => {
5
+ expect(utLayersToText([
6
+ {
7
+ name: "circle",
8
+ effects: {},
9
+ blendMode: "normal",
10
+ isClippingMask: false,
11
+ kind: "pixel",
12
+ visible: true,
13
+ },
14
+ ])).toMatchInlineSnapshot(`"◯ circle"`)
15
+ });
16
+
17
+ it("converts a nested layer to text", () => {
18
+ expect(utLayersToText([
19
+ {
20
+ name: "group",
21
+ effects: {},
22
+ blendMode: "passThrough",
23
+ isClippingMask: false,
24
+ kind: "group",
25
+ visible: true,
26
+ layers: [
27
+ {
28
+ name: "circle",
29
+ effects: {},
30
+ blendMode: "normal",
31
+ isClippingMask: false,
32
+ kind: "pixel",
33
+ visible: true,
34
+ },
35
+ {
36
+ name: "square",
37
+ effects: {},
38
+ blendMode: "normal",
39
+ isClippingMask: false,
40
+ kind: "pixel",
41
+ visible: true,
42
+ },
43
+ {
44
+ name: "other",
45
+ effects: {},
46
+ blendMode: "passThrough",
47
+ isClippingMask: false,
48
+ kind: "group",
49
+ visible: true,
50
+ layers: [
51
+ {
52
+ name: "nested",
53
+ effects: {},
54
+ blendMode: "normal",
55
+ isClippingMask: false,
56
+ kind: "pixel",
57
+ visible: true,
58
+ },
59
+ ],
60
+ },
61
+ ],
62
+ },
63
+ ])).toMatchInlineSnapshot(`
64
+ "◯ ▾ group
65
+ ◯ circle
66
+ ◯ square
67
+ ◯ ▾ other
68
+ ◯ nested"
69
+ `)
70
+ });
71
+
72
+ it("converts a nested layer with a clipping mask to text", () => {
73
+ expect(utLayersToText([
74
+ {
75
+ name: "clipper",
76
+ effects: {},
77
+ blendMode: "normal",
78
+ isClippingMask: true,
79
+ kind: "pixel",
80
+ visible: true,
81
+ },
82
+ {
83
+ name: "circle",
84
+ effects: {},
85
+ blendMode: "normal",
86
+ isClippingMask: false,
87
+ kind: "pixel",
88
+ visible: true,
89
+ },
90
+ ])).toMatchInlineSnapshot(`
91
+ "◯ ⬐ clipper
92
+ ◯ circle"
93
+ `)
94
+ });
@@ -0,0 +1,32 @@
1
+ import type { UTLayerPickKeys } from "../util/utLayerPickKeysType";
2
+ import type { UTLayer } from "./photoshopLayerDescriptorsToUTLayers";
3
+
4
+ const VISIBLE_ICON = "◯";
5
+ const INVISIBLE_ICON = "⊘";
6
+ const CLIPPING_MASK_ICON = "⬐";
7
+ const GROUP_ICON = "▾";
8
+ const EFFECTS_ICON = "ƒ";
9
+ const BLEND_ICON = "⁕"
10
+
11
+ type MinimalUTLayer = UTLayerPickKeys<"effects" | "visible" | "isClippingMask" | "kind" | "blendMode" | "name">;
12
+
13
+ export function utLayersToText(tree: MinimalUTLayer[], depth = 0): string {
14
+ return tree.map((layer) => {
15
+ const prefix = " ".repeat(depth * 2);
16
+ const name = layer.name;
17
+ const effects = Object.keys(layer.effects).length > 0 ? EFFECTS_ICON : "";
18
+ const blend = isSpecialBlendMode(layer) ? BLEND_ICON : "";
19
+ const clippingMask = layer.isClippingMask ? CLIPPING_MASK_ICON : "";
20
+ const group = layer.kind === "group" ? GROUP_ICON : "";
21
+ const visible = layer.visible ? VISIBLE_ICON : INVISIBLE_ICON;
22
+ const line = [visible, prefix, clippingMask, group, name, effects, blend].filter(Boolean).join(" ");
23
+ if (layer.layers) {
24
+ return line + "\n" + utLayersToText(layer.layers, depth + 1);
25
+ }
26
+ return line;
27
+ }).join("\n");
28
+ }
29
+
30
+ function isSpecialBlendMode(layer: Pick<UTLayer, "kind" | "blendMode">): boolean {
31
+ return layer.kind === "group" ? layer.blendMode !== "passThrough" : layer.blendMode !== "normal";
32
+ }
@@ -14,8 +14,9 @@ export function utLayersToTree(layer: UTLayer[]): Tree<UTLayerWithoutChildren> {
14
14
  blendMode: layer.blendMode,
15
15
  isClippingMask: layer.isClippingMask,
16
16
  effects: layer.effects,
17
+ background: layer.background,
17
18
  },
18
19
  name: layer.name,
19
20
  children: layer.layers ? utLayersToTree(layer.layers) : undefined,
20
21
  }));
21
- }
22
+ }
@@ -0,0 +1,5 @@
1
+ import type { UTLayer } from "../ut-tree/photoshopLayerDescriptorsToUTLayers";
2
+
3
+ export type UTLayerPickKeys<TKey extends keyof Omit<UTLayer, "layers">> = Pick<Omit<UTLayer, "layers">, TKey> & {
4
+ layers?: UTLayerPickKeys<TKey>[];
5
+ }
package/test/index.ts CHANGED
@@ -5,9 +5,10 @@ import { executeAsModalErrorTest } from "./meta-tests/executeAsModal.uxp-test";
5
5
  import { suspendHistoryTest } from "../src/core/suspendHistory.uxp-test";
6
6
  import { sourcemapsTest } from "../src/error-sourcemaps/sourcemaps.uxp-test";
7
7
  import { clipboardTest } from "../src/other/clipboard.uxp-test";
8
- import { photoshopLayerDescriptorsToUTLayersTest } from "../src/ut-tree/photoshopLayerDescriptorsToUTLayers.uxp-test";
8
+ import { photoshopLayerDescriptorsToUTLayersTest, photoshopLayerDescriptorsToUTLayersTest2 } from "../src/ut-tree/photoshopLayerDescriptorsToUTLayers.uxp-test";
9
9
  import { metadataStorageTest } from "../src/metadata-storage/metadataStorage.uxp-test";
10
10
  import { builtinModulesTest } from "./meta-tests/builtinModules.uxp-test";
11
+ import { backgroundLayerTest } from "../src/ut-tree/hasBackgroundLayer.uxp-test";
11
12
 
12
13
  export const tests = [
13
14
  applicationInfoTest,
@@ -18,6 +19,8 @@ export const tests = [
18
19
  sourcemapsTest,
19
20
  clipboardTest,
20
21
  photoshopLayerDescriptorsToUTLayersTest,
22
+ photoshopLayerDescriptorsToUTLayersTest2,
23
+ backgroundLayerTest,
21
24
  metadataStorageTest,
22
25
  builtinModulesTest,
23
26
  ];
@@ -0,0 +1,41 @@
1
+ import { Plugin } from "vitest/config";
2
+ import dedent from "dedent";
3
+
4
+ const nativeModules = [
5
+ "photoshop",
6
+ "uxp",
7
+ ];
8
+
9
+ const pluginPrefix = "photoshop-builtin"
10
+
11
+ const minimalModules = {
12
+ photoshop: dedent`
13
+ export const core = UNIMPLEMENTED;
14
+ export const action = UNIMPLEMENTED;
15
+ export const app = UNIMPLEMENTED;
16
+ `,
17
+ uxp: dedent`
18
+ export const entrypoints = UNIMPLEMENTED;
19
+ export const storage = UNIMPLEMENTED;
20
+ `,
21
+ }
22
+
23
+ export const vitestPhotoshopAliasPlugin = (): Plugin => ({
24
+ name: "vitest-photoshop-alias",
25
+ resolveId(id) {
26
+ if (nativeModules.includes(id)) {
27
+ return `${id}?${pluginPrefix}`
28
+ }
29
+ },
30
+ load(id) {
31
+ if (id.endsWith(`?${pluginPrefix}`)) {
32
+ const origModuleId = id.replace(`?${pluginPrefix}`, "");
33
+ const mod = dedent`
34
+ // This is a photoshop builtin module, not available in Vitest. This is an alias to provide minimal compatibility.
35
+ const UNIMPLEMENTED = { __vitest_photoshop_alias_unimplemented__: true };
36
+ ${minimalModules[origModuleId]}
37
+ `;
38
+ return mod;
39
+ }
40
+ }
41
+ });
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from "vitest/config";
2
+ import { vitestPhotoshopAliasPlugin } from "./vitest-photoshop-alias-plugin";
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ include: ["src/**/*.test.ts"],
7
+ reporters: process.env.CI ? ["default", "junit"] : ["default"],
8
+ outputFile: {
9
+ junit: "./test-results/junit.xml",
10
+ },
11
+ typecheck: {
12
+ enabled: true,
13
+ include: ["src/**/*.test-d.ts"],
14
+ },
15
+ },
16
+ plugins: [vitestPhotoshopAliasPlugin()],
17
+ });
@@ -1,32 +0,0 @@
1
- import type { Document } from "photoshop/dom/Document";
2
- import { z } from "zod";
3
- import { batchPlayCommand, createCommand } from "../core/command";
4
-
5
- // get layer properties like name and layerID for all layers in the document (by index)
6
- export const getLayerProperties = async (document: Document) => {
7
- const command = createCommand({
8
- modifying: false,
9
- descriptor: {
10
- _obj: "multiGet",
11
- _target: { _ref: [{ _ref: "document", _id: document.id }] },
12
- extendedReference: [
13
- ["name", "layerID", "visible"],
14
- { _obj: "layer", index: 1, count: -1 },
15
- ],
16
- },
17
- schema: z.object({
18
- list: z.array(
19
- z.object({
20
- name: z.string(),
21
- layerID: z.number(),
22
- visible: z.boolean().optional(),
23
- })
24
- )
25
- })
26
- });
27
-
28
- const result = await batchPlayCommand(command);
29
-
30
- // Reverse to get bottom-up order
31
- return [...result.list].reverse();
32
- };
@@ -1,72 +0,0 @@
1
- import { getLayerProperties } from "./getLayerProperties";
2
- import { z } from "zod";
3
- import { batchPlayCommands, createCommand } from "../core/command";
4
-
5
- // export interface LayerDescriptor {
6
- // name: string;
7
- // layerID: number;
8
- // layerSection?: string | { _value: string; _enum: string };
9
- // [key: string]: any;
10
- // }
11
-
12
- const layerDescriptorSchema = z.object({
13
- name: z.string(),
14
- // id: z.number(),
15
- layerID: z.number(),
16
- // _docId: z.number(),
17
- mode: z.object({
18
- _enum: z.literal("blendMode"),
19
- _value: z.string(), // passThrough, normal, multiply, screen, overlay, etc.
20
- }),
21
- background: z.boolean(),
22
- itemIndex: z.number(),
23
- visible: z.boolean(),
24
- layerKind: z.number(),
25
- layerSection: z.object({
26
- _value: z.enum([
27
- "layerSectionStart",
28
- "layerSectionEnd",
29
- "layerSectionContent",
30
- ]),
31
- _enum: z.literal("layerSectionType"),
32
- }),
33
- });
34
-
35
- export type LayerDescriptor = z.infer<typeof layerDescriptorSchema> & {
36
- docId: number;
37
- };
38
-
39
- // get all layers (including nested in groups)
40
- export const getFlattenedLayerDescriptorsList = async (
41
- documentId: number
42
- ) => {
43
- const layerProperties = await getLayerProperties(documentId);
44
-
45
- const commands = layerProperties.map((layerProp) =>
46
- createCommand({
47
- modifying: false,
48
- descriptor: {
49
- _obj: "get",
50
- _target: [
51
- {
52
- _ref: "layer",
53
- _id: layerProp.layerID,
54
- },
55
- ],
56
- makeVisible: false,
57
- layerID: [layerProp.layerID],
58
- _isCommand: false,
59
- },
60
- schema: layerDescriptorSchema,
61
- })
62
- );
63
-
64
- const layerDescriptors = await batchPlayCommands(commands);
65
-
66
- return layerDescriptors.map((desc) => {
67
- return {
68
- ...desc,
69
- docId: documentId,
70
- };
71
- });
72
- };
@@ -1,35 +0,0 @@
1
- import { z } from "zod";
2
- import { batchPlayCommand, createCommand } from "../core/command";
3
-
4
- export function createGetLayerPropertiesCommand(docId: number) {
5
- return createCommand({
6
- modifying: false,
7
- descriptor: {
8
- _obj: "multiGet",
9
- _target: { _ref: [{ _ref: "document", _id: docId }] },
10
- extendedReference: [
11
- ["name", "layerID", "visible"],
12
- { _obj: "layer", index: 1, count: -1 },
13
- ],
14
- },
15
- schema: z.object({
16
- list: z.array(
17
- z.object({
18
- name: z.string(),
19
- layerID: z.number(),
20
- visible: z.boolean().optional(),
21
- })
22
- )
23
- })
24
- });
25
- }
26
-
27
- // get layer properties like name and layerID for all layers in the document (by index)
28
- export const getLayerProperties = async (documentId: number) => {
29
- const command = createGetLayerPropertiesCommand(documentId);
30
-
31
- const result = await batchPlayCommand(command);
32
-
33
- // Reverse to get bottom-up order
34
- return [...result.list].reverse();
35
- };