editmamei 0.24.1 → 1.0.0
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.
- package/NOTICES.md +6 -30
- package/README.md +39 -7
- package/dist/api/extendscript/_helpers.js +1 -1
- package/dist/api/photoshop-api.js +20 -14
- package/dist/bin/editmamei-core-darwin-arm64 +0 -0
- package/dist/bin/editmamei-core-darwin-x64 +0 -0
- package/dist/bin/editmamei-core-win-x64.exe +0 -0
- package/dist/core/server.js +6 -6
- package/dist/core/session.js +9 -10
- package/dist/core/tool-registry.js +4 -4
- package/dist/delivery/store.js +10 -2
- package/dist/detection/detect-active-doc.js +1 -1
- package/dist/kernel/module-lifecycle.js +7 -1
- package/dist/perception/region-precompute.js +13 -5
- package/dist/perception/select-recipes.js +2 -2
- package/dist/perception/sky-mask-transfer.js +1 -1
- package/dist/platform/connection.js +32 -46
- package/dist/platform/host-platform.js +16 -0
- package/dist/platform/macos-detector.js +41 -71
- package/dist/platform/macos-runner.js +94 -0
- package/dist/platform/script-result.js +39 -0
- package/dist/platform/windows-detector.js +44 -68
- package/dist/platform/windows-runner.js +84 -0
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/tools/adjustment-tools.js +18 -2
- package/dist/tools/brush-tools.js +1 -1
- package/dist/tools/document-tools.js +9 -2
- package/dist/tools/filter-tools.js +1 -1
- package/dist/tools/layer-tools.js +132 -7
- package/dist/tools/path-tools.js +1 -1
- package/dist/tools/preview-tools.js +8 -8
- package/dist/tools/retouch-tools.js +1 -1
- package/dist/tools/scene-tools.js +29 -2
- package/dist/tools/selection-tools.js +81 -3
- package/dist/update/check.js +1 -1
- package/dist/utils/logger.js +53 -46
- package/dist/utils/session-log.js +1 -1
- package/dist/utils/tool-helpers.js +9 -3
- package/dist/version.js +1 -1
- package/package.json +10 -3
- package/LICENSE +0 -81
- package/dist/platform/detector.js +0 -32
- package/dist/platform/macos-executor.js +0 -96
- package/dist/platform/windows-executor.js +0 -105
- /package/dist/platform/{script-executor.js → ports.js} +0 -0
- /package/dist/platform/{executor-queue.js → script-queue.js} +0 -0
|
@@ -75,7 +75,7 @@ const savePsdSchema = {
|
|
|
75
75
|
},
|
|
76
76
|
keep_scene_channels: {
|
|
77
77
|
type: 'boolean',
|
|
78
|
-
description: "Keep the managed scene:* alpha channels ps_read_scene precomputes. Default false — they are DERIVED masks (rebuilt by the next scene read) and each is a full-resolution channel, so baking them into the .psd bloats it badly (~51MB per channel on a 51MP document). Set true only if you want the saved file to carry the masks. The result reports scene_channels_purged either way, so it's never silent.",
|
|
78
|
+
description: "Keep the managed scene:* alpha channels ps_read_scene precomputes. Default false — they are DERIVED masks (rebuilt by the next scene read) and each is a full-resolution channel, so baking them into the .psd bloats it badly (~51MB per channel on a 51MP document). Set true only if you want the saved file to carry the masks. The result reports scene_channels_purged either way, so it's never silent. NOTE: the `scene:` channel-name prefix is RESERVED — the purge matches on the prefix alone, so a hand-made channel named e.g. `scene:mine` is deleted along with the derived ones. Name your own channels anything else.",
|
|
79
79
|
default: false,
|
|
80
80
|
},
|
|
81
81
|
},
|
|
@@ -415,8 +415,15 @@ async function openDocumentPipeline(connection, snippetClient, rawArgs) {
|
|
|
415
415
|
}
|
|
416
416
|
}
|
|
417
417
|
async function savePsdPipeline(connection, snippetClient, rawArgs) {
|
|
418
|
+
let args;
|
|
419
|
+
try {
|
|
420
|
+
args = validateArgs(savePsdSchema, rawArgs);
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
return toolErrorResult('Error saving PSD', error);
|
|
424
|
+
}
|
|
418
425
|
let purged = 0;
|
|
419
|
-
if (
|
|
426
|
+
if (args.keep_scene_channels !== true) {
|
|
420
427
|
try {
|
|
421
428
|
purged = await purgeSceneChannels(connection);
|
|
422
429
|
}
|
|
@@ -928,7 +928,7 @@ export function createFilterTools(connection, snippetClient, client = new OnnxLa
|
|
|
928
928
|
{
|
|
929
929
|
tool: {
|
|
930
930
|
name: 'ps_apply_filter',
|
|
931
|
-
description: 'Apply a Photoshop filter to a DUPLICATE of the active layer by default (auto-duplicate-first
|
|
931
|
+
description: 'Apply a Photoshop filter to a DUPLICATE of the active layer by default (auto-duplicate-first — the original is preserved, undo by deleting the copy). Pass `apply_to_active_layer: true` to bake into the original. Auto-rasterizes text/smart-object layers. Choose the filter with `type`; each type takes its own parameters (see the `type` field). Covers blur (gaussian_blur/motion_blur/lens_blur/radial_blur), sharpen (`sharpen`=Unsharp Mask, `smart_sharpen`), noise (`noise`=Add Noise, `reduce_noise`), high_pass, pixelate, distort, displace, and oil_paint.',
|
|
932
932
|
inputSchema: FILTER_INPUT_SCHEMA,
|
|
933
933
|
outputSchema: FILTER_OUTPUT_SCHEMA,
|
|
934
934
|
annotations: {
|
|
@@ -79,20 +79,106 @@ const addFillLayerSchema = {
|
|
|
79
79
|
properties: {
|
|
80
80
|
fill_type: {
|
|
81
81
|
type: 'string',
|
|
82
|
-
enum: ['solid_color'],
|
|
83
|
-
description: 'Fill-layer type.
|
|
82
|
+
enum: ['solid_color', 'gradient'],
|
|
83
|
+
description: 'Fill-layer type. solid_color takes red/green/blue (required for that type); gradient takes the gradient_* params + stops (pattern fills still planned).',
|
|
84
84
|
default: 'solid_color',
|
|
85
85
|
},
|
|
86
|
-
red: {
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
red: {
|
|
87
|
+
type: 'integer',
|
|
88
|
+
description: 'fill_type=solid_color: red (0-255). Required for solid_color.',
|
|
89
|
+
minimum: 0,
|
|
90
|
+
maximum: 255,
|
|
91
|
+
},
|
|
92
|
+
green: {
|
|
93
|
+
type: 'integer',
|
|
94
|
+
description: 'fill_type=solid_color: green (0-255). Required for solid_color.',
|
|
95
|
+
minimum: 0,
|
|
96
|
+
maximum: 255,
|
|
97
|
+
},
|
|
98
|
+
blue: {
|
|
99
|
+
type: 'integer',
|
|
100
|
+
description: 'fill_type=solid_color: blue (0-255). Required for solid_color.',
|
|
101
|
+
minimum: 0,
|
|
102
|
+
maximum: 255,
|
|
103
|
+
},
|
|
104
|
+
gradient_type: {
|
|
105
|
+
type: 'string',
|
|
106
|
+
enum: ['linear', 'radial', 'angle', 'reflected', 'diamond'],
|
|
107
|
+
description: 'fill_type=gradient: gradient geometry. linear for skies/fades, radial for vignettes/glows.',
|
|
108
|
+
default: 'linear',
|
|
109
|
+
},
|
|
110
|
+
angle: {
|
|
111
|
+
type: 'number',
|
|
112
|
+
minimum: -180,
|
|
113
|
+
maximum: 180,
|
|
114
|
+
description: 'fill_type=gradient: gradient angle in degrees (Photoshop convention: 90 runs the first stop from the bottom up; 0 runs it left to right).',
|
|
115
|
+
default: 90,
|
|
116
|
+
},
|
|
117
|
+
scale: {
|
|
118
|
+
type: 'number',
|
|
119
|
+
minimum: 10,
|
|
120
|
+
maximum: 150,
|
|
121
|
+
description: 'fill_type=gradient: gradient scale percent (compress/stretch the ramp).',
|
|
122
|
+
default: 100,
|
|
123
|
+
},
|
|
124
|
+
reverse: {
|
|
125
|
+
type: 'boolean',
|
|
126
|
+
description: 'fill_type=gradient: reverse the stop order.',
|
|
127
|
+
default: false,
|
|
128
|
+
},
|
|
129
|
+
dither: {
|
|
130
|
+
type: 'boolean',
|
|
131
|
+
description: 'fill_type=gradient: dither to reduce banding.',
|
|
132
|
+
default: true,
|
|
133
|
+
},
|
|
134
|
+
offset_x: {
|
|
135
|
+
type: 'number',
|
|
136
|
+
minimum: -100,
|
|
137
|
+
maximum: 100,
|
|
138
|
+
description: 'fill_type=gradient: horizontal center offset percent.',
|
|
139
|
+
default: 0,
|
|
140
|
+
},
|
|
141
|
+
offset_y: {
|
|
142
|
+
type: 'number',
|
|
143
|
+
minimum: -100,
|
|
144
|
+
maximum: 100,
|
|
145
|
+
description: 'fill_type=gradient: vertical center offset percent.',
|
|
146
|
+
default: 0,
|
|
147
|
+
},
|
|
148
|
+
stops: {
|
|
149
|
+
type: 'array',
|
|
150
|
+
description: 'fill_type=gradient: color stops, each {red,green,blue (0-255), location (0-100 along the ramp), midpoint (5-95, default 50)}. At least 2 when supplied; sorted by location. Default: black at 0 → white at 100.',
|
|
151
|
+
items: {
|
|
152
|
+
type: 'object',
|
|
153
|
+
properties: {
|
|
154
|
+
red: { type: 'integer', minimum: 0, maximum: 255 },
|
|
155
|
+
green: { type: 'integer', minimum: 0, maximum: 255 },
|
|
156
|
+
blue: { type: 'integer', minimum: 0, maximum: 255 },
|
|
157
|
+
location: { type: 'number', minimum: 0, maximum: 100 },
|
|
158
|
+
midpoint: { type: 'number', minimum: 5, maximum: 95, default: 50 },
|
|
159
|
+
},
|
|
160
|
+
required: ['red', 'green', 'blue', 'location'],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
opacity_stops: {
|
|
164
|
+
type: 'array',
|
|
165
|
+
description: 'fill_type=gradient: opacity stops, each {opacity (0-100), location (0-100), midpoint (5-95, default 50)}. Use e.g. 100→0 for a fade-to-transparent wash. Default: fully opaque.',
|
|
166
|
+
items: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
properties: {
|
|
169
|
+
opacity: { type: 'number', minimum: 0, maximum: 100 },
|
|
170
|
+
location: { type: 'number', minimum: 0, maximum: 100 },
|
|
171
|
+
midpoint: { type: 'number', minimum: 5, maximum: 95, default: 50 },
|
|
172
|
+
},
|
|
173
|
+
required: ['opacity', 'location'],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
89
176
|
into_active_group: {
|
|
90
177
|
type: 'boolean',
|
|
91
178
|
description: "Photoshop's Mk-contentLayer descriptor carries no placement target, so with a GROUP active it would natively nest the new fill layer INSIDE that group. Default false hoists the new layer back out so it lands above the active layer/group as a sibling. Pass true to keep it nested inside the active group instead.",
|
|
92
179
|
default: false,
|
|
93
180
|
},
|
|
94
181
|
},
|
|
95
|
-
required: ['red', 'green', 'blue'],
|
|
96
182
|
};
|
|
97
183
|
const selectLayerSchema = {
|
|
98
184
|
type: 'object',
|
|
@@ -198,7 +284,7 @@ export function createLayerTools(connection, snippetClient) {
|
|
|
198
284
|
{
|
|
199
285
|
tool: {
|
|
200
286
|
name: 'ps_add_fill_layer',
|
|
201
|
-
description: "Add a non-destructive SOLID COLOR fill layer (an editable
|
|
287
|
+
description: "Add a non-destructive SOLID COLOR or GRADIENT fill layer (an editable content layer — distinct from ps_fill_layer, which bakes color into the active pixel layer). fill_type=gradient is the go-to for sky fades, color washes, and vignettes (radial + reverse + multiply blend); combine with opacity_stops 100→0 for fade-to-transparent, or ps_bake_layer to rasterize. For fading a layer out via its MASK use ps_layer_mask op=gradient instead. Hoisted out of the active layer's group by default (pass into_active_group:true to keep Photoshop's native nesting). The new fill layer becomes active.",
|
|
202
288
|
inputSchema: addFillLayerSchema,
|
|
203
289
|
outputSchema: {
|
|
204
290
|
type: 'object',
|
|
@@ -206,6 +292,11 @@ export function createLayerTools(connection, snippetClient) {
|
|
|
206
292
|
created: { type: 'boolean' },
|
|
207
293
|
fill_type: { type: 'string' },
|
|
208
294
|
color: { type: 'object' },
|
|
295
|
+
gradient_type: { type: 'string' },
|
|
296
|
+
angle: { type: 'number' },
|
|
297
|
+
scale: { type: 'number' },
|
|
298
|
+
reverse: { type: 'boolean' },
|
|
299
|
+
stop_count: { type: 'number' },
|
|
209
300
|
layer_name: { type: 'string' },
|
|
210
301
|
hoisted: {
|
|
211
302
|
type: 'boolean',
|
|
@@ -325,6 +416,40 @@ async function fillLayer(connection, snippetClient, rawArgs) {
|
|
|
325
416
|
});
|
|
326
417
|
}
|
|
327
418
|
async function addFillLayer(connection, snippetClient, rawArgs) {
|
|
419
|
+
if (rawArgs.fill_type === 'gradient') {
|
|
420
|
+
return runSnippetTool({
|
|
421
|
+
connection,
|
|
422
|
+
snippetClient,
|
|
423
|
+
rawArgs,
|
|
424
|
+
schema: addFillLayerSchema,
|
|
425
|
+
snippet: 'addGradientFillLayer',
|
|
426
|
+
errorPrefix: 'Error adding gradient fill layer',
|
|
427
|
+
params: (args) => {
|
|
428
|
+
const params = {
|
|
429
|
+
gradient_type: args.gradient_type ?? 'linear',
|
|
430
|
+
angle: args.angle ?? 90,
|
|
431
|
+
scale: args.scale ?? 100,
|
|
432
|
+
reverse: args.reverse ?? false,
|
|
433
|
+
dither: args.dither ?? true,
|
|
434
|
+
offset_x: args.offset_x ?? 0,
|
|
435
|
+
offset_y: args.offset_y ?? 0,
|
|
436
|
+
into_active_group: args.into_active_group ?? false,
|
|
437
|
+
};
|
|
438
|
+
if (args.stops !== undefined)
|
|
439
|
+
params.stops = args.stops;
|
|
440
|
+
if (args.opacity_stops !== undefined)
|
|
441
|
+
params.opacity_stops = args.opacity_stops;
|
|
442
|
+
return params;
|
|
443
|
+
},
|
|
444
|
+
successText: (result, args) => {
|
|
445
|
+
const stopCount = result.stop_count;
|
|
446
|
+
return `Gradient fill layer added: ${args.gradient_type ?? 'linear'}, angle ${args.angle ?? 90}${stopCount !== undefined ? `, ${stopCount} stops` : ''}`;
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
if (rawArgs.red === undefined || rawArgs.green === undefined || rawArgs.blue === undefined) {
|
|
451
|
+
return toolErrorResult('Error adding fill layer', new Error('fill_type=solid_color requires red, green, and blue (0-255).'));
|
|
452
|
+
}
|
|
328
453
|
return runSnippetTool({
|
|
329
454
|
connection,
|
|
330
455
|
snippetClient,
|
package/dist/tools/path-tools.js
CHANGED
|
@@ -113,7 +113,7 @@ export function createPathTools(connection, snippetClient, client = new OnnxLand
|
|
|
113
113
|
{
|
|
114
114
|
tool: {
|
|
115
115
|
name: 'ps_path',
|
|
116
|
-
description: 'Work with editable vector PATHS — the connective tissue between selections, brush strokes, and clipping. One op-discriminated tool: `create_from_selection` (selection → work path), `create_from_placement` (NAME a curve → editable saved path, the grounded pen), `save` (name the work path), `list`, `delete`, `load_as_selection` (path → selection), `stroke` (paint a brush-family tool along a path), `fill` (fill a path region), `set_clipping` (mark a saved path as the clipping path). **Reach for this when**: tracing a named curve (an edge, a landmark contour) into an editable path, turning a precise selection into a reusable/strokeable outline, converting a saved path back to a selection, or outlining/filling a shape exactly. create_from_selection and load_as_selection are an exact round-trip. stroke/fill bake pixels and auto-duplicate the target layer
|
|
116
|
+
description: 'Work with editable vector PATHS — the connective tissue between selections, brush strokes, and clipping. One op-discriminated tool: `create_from_selection` (selection → work path), `create_from_placement` (NAME a curve → editable saved path, the grounded pen), `save` (name the work path), `list`, `delete`, `load_as_selection` (path → selection), `stroke` (paint a brush-family tool along a path), `fill` (fill a path region), `set_clipping` (mark a saved path as the clipping path). **Reach for this when**: tracing a named curve (an edge, a landmark contour) into an editable path, turning a precise selection into a reusable/strokeable outline, converting a saved path back to a selection, or outlining/filling a shape exactly. create_from_selection and load_as_selection are an exact round-trip. stroke/fill bake pixels and auto-duplicate the target layer. Every op returns path_info (counts + per-path subpath/anchor totals) so you can verify the path landed; load_as_selection also returns selection_info.',
|
|
117
117
|
inputSchema: pathInputSchema,
|
|
118
118
|
outputSchema: {
|
|
119
119
|
type: 'object',
|
|
@@ -200,7 +200,7 @@ export function createPreviewTools(connection, snippetClient) {
|
|
|
200
200
|
{
|
|
201
201
|
tool: {
|
|
202
202
|
name: 'ps_get_preview',
|
|
203
|
-
description: 'Render the active Photoshop document as a flattened, downscaled image and return it inline so the calling AI can see the current visual state. Use this to verify edits visually, judge whether adjustments went too far or not far enough, and decide what to do next. Read-only with respect to the working document (renders a duplicate that is closed immediately). The optional `annotations` array draws overlays in document-space coordinates: `rectangle` / `guide` / `point` / `selection` for spatial verification
|
|
203
|
+
description: 'Render the active Photoshop document as a flattened, downscaled image and return it inline so the calling AI can see the current visual state. Use this to verify edits visually, judge whether adjustments went too far or not far enough, and decide what to do next. Read-only with respect to the working document (renders a duplicate that is closed immediately). The optional `annotations` array draws overlays in document-space coordinates: `rectangle` / `guide` / `point` / `selection` for spatial verification, and `grid` / `composition` for coordinate readout and compositional evaluation (added 2026-06-09 — grid styles `every` / `thirds` / `quarters` / `phi`, composition styles `diagonals` / `triangles` / `fibonacci_grid` / `golden_spiral`). Use spatial annotations to verify ("did the placed image land inside the frame interior?") — pass both target and actual bounds in different colors. Use grid / composition annotations to EVALUATE composition (balance, thirds, leading lines, focal-point placement) — not to read a coordinate for a tool call. To place or select precisely, do NOT read a pixel off a grid (unreliable) — NAME the intent to the on-device perception tools instead: `ps_detect` (COCO objects → real document-pixel boxes), `ps_select_by_reference` (a named target → a real selection, not a guessed rectangle), plus any spatial-grounding locator / selection tools present in `tools/list`. Without annotations, behaves identically to a plain preview.',
|
|
204
204
|
inputSchema: previewSchema,
|
|
205
205
|
outputSchema: {
|
|
206
206
|
type: 'object',
|
|
@@ -224,7 +224,7 @@ export function createPreviewTools(connection, snippetClient) {
|
|
|
224
224
|
{
|
|
225
225
|
tool: {
|
|
226
226
|
name: 'ps_get_layer_bounds_diff',
|
|
227
|
-
description: 'Numeric verification: compute the per-edge pixel deltas between a layer\'s actual bounds and a target rectangle. Returns left/top/right/bottom deltas (actual − target), scale ratio (actual_size / target_size), centroid offset, and a one-word verdict ("aligned", "shifted right", "layer too small", etc.). Use this AFTER place_image / scale_layer / move_layer to confirm the operation produced the intended result — far more reliable than eyeballing a preview for fine alignment. Read-only. Layers nested in groups are found by name recursively.
|
|
227
|
+
description: 'Numeric verification: compute the per-edge pixel deltas between a layer\'s actual bounds and a target rectangle. Returns left/top/right/bottom deltas (actual − target), scale ratio (actual_size / target_size), centroid offset, and a one-word verdict ("aligned", "shifted right", "layer too small", etc.). Use this AFTER place_image / scale_layer / move_layer to confirm the operation produced the intended result — far more reliable than eyeballing a preview for fine alignment. Read-only. Layers nested in groups are found by name recursively.',
|
|
228
228
|
inputSchema: boundsDiffSchema,
|
|
229
229
|
outputSchema: {
|
|
230
230
|
type: 'object',
|
|
@@ -283,7 +283,7 @@ export function createPreviewTools(connection, snippetClient) {
|
|
|
283
283
|
{
|
|
284
284
|
tool: {
|
|
285
285
|
name: 'ps_compare_regions',
|
|
286
|
-
description: 'Quantitative region comparison — the NUMERIC verification answer when "does it look right?" via get_preview is too subjective. Captures the histogram of two rectangular regions on the same document and reports per-channel mean / stdev / median for each region plus the deltas. **Reach for this when**: (a) "did the placed image match the room\'s lighting?" — sample a region of the placed image vs a region of the surrounding wall; (b) "did this adjustment do anything visible in the shadows?" — sample the same dark region before and after; (c) "is region A noticeably warmer/cooler than region B?" — read the channel means directly. Single-pixel sampling via a 1×1 rect is supported. Read-only.
|
|
286
|
+
description: 'Quantitative region comparison — the NUMERIC verification answer when "does it look right?" via get_preview is too subjective. Captures the histogram of two rectangular regions on the same document and reports per-channel mean / stdev / median for each region plus the deltas. **Reach for this when**: (a) "did the placed image match the room\'s lighting?" — sample a region of the placed image vs a region of the surrounding wall; (b) "did this adjustment do anything visible in the shadows?" — sample the same dark region before and after; (c) "is region A noticeably warmer/cooler than region B?" — read the channel means directly. Single-pixel sampling via a 1×1 rect is supported. Read-only.',
|
|
287
287
|
inputSchema: compareRegionsSchema,
|
|
288
288
|
outputSchema: {
|
|
289
289
|
type: 'object',
|
|
@@ -359,7 +359,7 @@ function buildAnnotationsScript(annotations) {
|
|
|
359
359
|
}
|
|
360
360
|
}
|
|
361
361
|
return `
|
|
362
|
-
// ANNOTATIONS BLOCK
|
|
362
|
+
// ANNOTATIONS BLOCK
|
|
363
363
|
(function () {
|
|
364
364
|
var docW = dup.width.as('px');
|
|
365
365
|
var docH = dup.height.as('px');
|
|
@@ -643,7 +643,7 @@ function buildAnnotationsScript(annotations) {
|
|
|
643
643
|
// it's just not a mathematically-perfect golden spiral when
|
|
644
644
|
// the canvas isn't golden. maxLevels capped at 8 to keep the
|
|
645
645
|
// total fill count bounded for the 90s annotated-preview
|
|
646
|
-
// timeout
|
|
646
|
+
// timeout.
|
|
647
647
|
function drawFibonacciOverlay(startCorner, c, sw, drawSpiral) {
|
|
648
648
|
try {
|
|
649
649
|
// Track the active rectangle as we shrink it.
|
|
@@ -779,7 +779,7 @@ async function getPreview(connection, rawArgs) {
|
|
|
779
779
|
${getContextInfo}
|
|
780
780
|
|
|
781
781
|
if (app.documents.length === 0) {
|
|
782
|
-
throw new Error('No
|
|
782
|
+
throw new Error('No document is open in Photoshop');
|
|
783
783
|
}
|
|
784
784
|
// Defensive cleanup: a prior killed wrapper.vbs (annotated previews
|
|
785
785
|
// timing out at 30s) leaves a stale "<orig> __mcp_preview__" doc
|
|
@@ -895,7 +895,7 @@ async function getLayerBoundsDiff(connection, rawArgs) {
|
|
|
895
895
|
const script = `
|
|
896
896
|
${getContextInfo}
|
|
897
897
|
|
|
898
|
-
if (app.documents.length === 0) throw new Error('No
|
|
898
|
+
if (app.documents.length === 0) throw new Error('No document is open in Photoshop');
|
|
899
899
|
var doc = app.activeDocument;
|
|
900
900
|
|
|
901
901
|
${normNameHelper}
|
|
@@ -1037,7 +1037,7 @@ async function compareRegions(connection, rawArgs) {
|
|
|
1037
1037
|
const script = `
|
|
1038
1038
|
${getContextInfo}
|
|
1039
1039
|
|
|
1040
|
-
if (app.documents.length === 0) throw new Error('No
|
|
1040
|
+
if (app.documents.length === 0) throw new Error('No document is open in Photoshop');
|
|
1041
1041
|
var doc = app.activeDocument;
|
|
1042
1042
|
|
|
1043
1043
|
function statsForRect(left, top, right, bottom) {
|
|
@@ -370,7 +370,7 @@ export function createRetouchTools(connection, snippetClient, detClient = new On
|
|
|
370
370
|
{
|
|
371
371
|
tool: {
|
|
372
372
|
name: 'ps_retouch',
|
|
373
|
-
description: 'Selection-driven content-aware retouch — choose the technique with `method`. Requires an active selection (throws if none, so Content-Aware Fill never silently fills the whole layer). Auto-duplicates the active layer by default so the original is preserved (auto-duplicate-first
|
|
373
|
+
description: 'Selection-driven content-aware retouch — choose the technique with `method`. Requires an active selection (throws if none, so Content-Aware Fill never silently fills the whole layer). Auto-duplicates the active layer by default so the original is preserved (auto-duplicate-first); pass `apply_to_active_layer: true` to bake into the original. `content_aware_fill` synthesizes a fill from surroundings (remove wires/objects/blemishes); `patch` samples replacement pixels from a chosen offset — or NAME the source region with `source_placement` (grounded + gated); `content_aware_move` relocates the selection and fills the source — offset, or NAME where with `destination_placement`.',
|
|
374
374
|
inputSchema: RETOUCH_INPUT_SCHEMA,
|
|
375
375
|
outputSchema: RETOUCH_OUTPUT_SCHEMA,
|
|
376
376
|
annotations: {
|
|
@@ -83,7 +83,7 @@ const sceneSchema = {
|
|
|
83
83
|
save_regions: {
|
|
84
84
|
type: 'boolean',
|
|
85
85
|
default: true,
|
|
86
|
-
description: 'Precompute every confident region (sky/ground/shadows/highlights/skin/subject/face — and, on a Pro host with a face, the face-feature set scene:face_skin/_eyes/_brows/_lips/_teeth/_nose/_under_eye/_cheeks) and SAVE each as a managed `scene:*` alpha channel, returning the `regions` MENU of what is confidently selectable (each with its method + confidence). ps_select_by_reference then loads the saved channel instantly. Set false for a light read with no channels.',
|
|
86
|
+
description: 'Precompute every confident region (sky/ground/shadows/highlights/skin/subject/face — and, on a Pro host with a face, the face-feature set scene:face_skin/_eyes/_brows/_lips/_teeth/_nose/_under_eye/_cheeks) and SAVE each as a managed `scene:*` alpha channel, returning the `regions` MENU of what is confidently selectable (each with its method + confidence). ps_select_by_reference then loads the saved channel instantly. Set false for a light read with no channels. The `scene:` channel-name prefix is RESERVED: channels matching it are treated as derived and are deleted on the next scene read and on ps_save_psd, so do not give a channel you want to keep a `scene:`-prefixed name.',
|
|
87
87
|
},
|
|
88
88
|
composition_context: {
|
|
89
89
|
type: 'object',
|
|
@@ -375,7 +375,34 @@ export function createSceneTools(connection, snippetClient, opts = {}) {
|
|
|
375
375
|
doc: { type: 'object' },
|
|
376
376
|
subjects: { type: 'array', items: { type: 'object' } },
|
|
377
377
|
faces: { type: 'array', items: { type: 'object' } },
|
|
378
|
-
regions: {
|
|
378
|
+
regions: {
|
|
379
|
+
type: 'array',
|
|
380
|
+
items: {
|
|
381
|
+
type: 'object',
|
|
382
|
+
properties: {
|
|
383
|
+
kind: { type: 'string' },
|
|
384
|
+
coverage: { type: 'number' },
|
|
385
|
+
coverage_is_estimate: {
|
|
386
|
+
type: 'boolean',
|
|
387
|
+
description: 'Always true today: `coverage` is the coarse histogram split, never a measured mask. It and the real selection routinely disagree (a night shot read sky at 0.08 while the gated selection found a real sky at 0.83) — trust `selectable_confidence` and the precomputed channel over this number. Carried as a field rather than left implicit so a future measured-coverage path can report false.',
|
|
388
|
+
},
|
|
389
|
+
selectable: {
|
|
390
|
+
type: ['boolean', 'null'],
|
|
391
|
+
description: 'Tristate as null when the region was never resolved — never read this as a plain boolean; branch on `selectable_state`.',
|
|
392
|
+
},
|
|
393
|
+
selectable_state: {
|
|
394
|
+
type: 'string',
|
|
395
|
+
enum: ['selectable', 'not_selectable', 'not_resolved'],
|
|
396
|
+
description: '`selectable`: a precomputed channel is ready to load. `not_selectable`: resolution ran and this region did not pass the confidence gate. `not_resolved`: precompute did not run or failed, so absence here is NOT evidence the region is unavailable.',
|
|
397
|
+
},
|
|
398
|
+
selectable_via: {
|
|
399
|
+
type: 'string',
|
|
400
|
+
description: 'The method that resolved it (only when selectable).',
|
|
401
|
+
},
|
|
402
|
+
selectable_confidence: { type: 'number' },
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
},
|
|
379
406
|
region_menu: { type: 'array', items: { type: 'object' } },
|
|
380
407
|
horizon: { type: 'object' },
|
|
381
408
|
tonal_zones: { type: 'object' },
|
|
@@ -522,7 +522,35 @@ const SELECTION_CHANNEL_INPUT_SCHEMA = {
|
|
|
522
522
|
},
|
|
523
523
|
required: ['op', 'channel_name'],
|
|
524
524
|
};
|
|
525
|
-
const LAYER_MASK_OPS = ['create', 'delete', 'apply'];
|
|
525
|
+
const LAYER_MASK_OPS = ['create', 'delete', 'apply', 'gradient'];
|
|
526
|
+
const MASK_GRADIENT_PROPS = {
|
|
527
|
+
fade_to: {
|
|
528
|
+
type: 'string',
|
|
529
|
+
enum: ['bottom', 'top', 'left', 'right'],
|
|
530
|
+
description: 'op=gradient: the side that ends fully HIDDEN (mask black). The opposite side stays fully visible. E.g. a water reflection fades with fade_to=bottom.',
|
|
531
|
+
default: 'bottom',
|
|
532
|
+
},
|
|
533
|
+
start: {
|
|
534
|
+
type: 'number',
|
|
535
|
+
minimum: 0,
|
|
536
|
+
maximum: 1,
|
|
537
|
+
description: 'op=gradient: 0-1 fraction along the fade direction where the fade begins — the layer stays fully visible up to here.',
|
|
538
|
+
default: 0,
|
|
539
|
+
},
|
|
540
|
+
end: {
|
|
541
|
+
type: 'number',
|
|
542
|
+
minimum: 0,
|
|
543
|
+
maximum: 1,
|
|
544
|
+
description: 'op=gradient: 0-1 fraction where the fade completes — fully hidden from here on. Must be greater than start.',
|
|
545
|
+
default: 1,
|
|
546
|
+
},
|
|
547
|
+
extent: {
|
|
548
|
+
type: 'string',
|
|
549
|
+
enum: ['layer', 'canvas'],
|
|
550
|
+
description: "op=gradient: measure start/end over the active layer's pixel bounds (default) or the whole canvas. A layer with no pixel bounds falls back to canvas.",
|
|
551
|
+
default: 'layer',
|
|
552
|
+
},
|
|
553
|
+
};
|
|
526
554
|
const LAYER_MASK_INPUT_SCHEMA = {
|
|
527
555
|
type: 'object',
|
|
528
556
|
properties: {
|
|
@@ -531,11 +559,17 @@ const LAYER_MASK_INPUT_SCHEMA = {
|
|
|
531
559
|
enum: [...LAYER_MASK_OPS],
|
|
532
560
|
description: 'create: add a layer mask on the active layer (if a selection is active it reveals the selection — the "mask to the frame opening" answer; adjustment layers load the selection into their built-in mask). ' +
|
|
533
561
|
'delete: remove the mask (layer pixels preserved). ' +
|
|
534
|
-
'apply: DESTRUCTIVE — bake the mask into pixels (pixels outside the mask are lost).'
|
|
562
|
+
'apply: DESTRUCTIVE — bake the mask into pixels (pixels outside the mask are lost). ' +
|
|
563
|
+
'gradient: draw a linear white→black fade INTO the mask (auto-creates a reveal-all mask if none) — the universal fade-out primitive (reflections, sky blends, edge feathering). REPLACES existing mask content.',
|
|
535
564
|
},
|
|
565
|
+
...MASK_GRADIENT_PROPS,
|
|
536
566
|
},
|
|
537
567
|
required: ['op'],
|
|
538
568
|
};
|
|
569
|
+
const MASK_GRADIENT_ARGS_SCHEMA = {
|
|
570
|
+
type: 'object',
|
|
571
|
+
properties: { ...MASK_GRADIENT_PROPS },
|
|
572
|
+
};
|
|
539
573
|
export function createSelectionTools(connection, snippetClient, client = new OnnxLandmarkDetectionClient()) {
|
|
540
574
|
return [
|
|
541
575
|
{
|
|
@@ -640,7 +674,7 @@ export function createSelectionTools(connection, snippetClient, client = new Onn
|
|
|
640
674
|
{
|
|
641
675
|
tool: {
|
|
642
676
|
name: 'ps_layer_mask',
|
|
643
|
-
description: 'Manage the active layer\'s mask — choose with `op`. `create`: add a layer mask; if a selection is active it reveals the selection and hides the rest (the one-call "mask the placed image to the frame opening" — make the selection first, then create); with no selection, a reveal-all mask; adjustment layers load the current selection into their built-in mask. `delete`: remove the mask (layer pixels preserved). `apply`: DESTRUCTIVE — permanently bake the mask into the pixels (outside-mask pixels lost). Prefer keeping masks non-destructive unless flattening for export. **Don\'t write `Mk Chnl At=Msk` AM scripts in execute_script — this tool already does
|
|
677
|
+
description: 'Manage the active layer\'s mask — choose with `op`. `create`: add a layer mask; if a selection is active it reveals the selection and hides the rest (the one-call "mask the placed image to the frame opening" — make the selection first, then create); with no selection, a reveal-all mask; adjustment layers load the current selection into their built-in mask. `delete`: remove the mask (layer pixels preserved). `apply`: DESTRUCTIVE — permanently bake the mask into the pixels (outside-mask pixels lost). `gradient`: draw a linear fade into the mask (fade_to names the side that ends hidden; start/end place the fade; auto-creates the mask; replaces existing mask content and clears any active selection first) — the standard fade for reflections, sky blends, and soft edge falloff. Verify with ps_get_preview. Prefer keeping masks non-destructive unless flattening for export. **Don\'t write `Mk Chnl At=Msk` or `Grdn` AM scripts in execute_script — this tool already does both.**',
|
|
644
678
|
inputSchema: LAYER_MASK_INPUT_SCHEMA,
|
|
645
679
|
outputSchema: {
|
|
646
680
|
type: 'object',
|
|
@@ -651,6 +685,26 @@ export function createSelectionTools(connection, snippetClient, client = new Onn
|
|
|
651
685
|
hadSelection: { type: 'boolean' },
|
|
652
686
|
maskDeleted: { type: 'boolean' },
|
|
653
687
|
maskApplied: { type: 'boolean' },
|
|
688
|
+
mask_gradient: { type: 'boolean' },
|
|
689
|
+
created_mask: {
|
|
690
|
+
type: 'boolean',
|
|
691
|
+
description: 'op=gradient: true when the reveal-all mask had to be created first.',
|
|
692
|
+
},
|
|
693
|
+
fade_to: { type: 'string' },
|
|
694
|
+
from: {
|
|
695
|
+
type: 'object',
|
|
696
|
+
description: 'op=gradient: fully-visible end of the drawn fade, doc pixels.',
|
|
697
|
+
},
|
|
698
|
+
to: {
|
|
699
|
+
type: 'object',
|
|
700
|
+
description: 'op=gradient: fully-hidden end of the drawn fade, doc pixels.',
|
|
701
|
+
},
|
|
702
|
+
extent: {
|
|
703
|
+
type: 'string',
|
|
704
|
+
description: "op=gradient: 'layer' or 'canvas' — reports the extent actually used.",
|
|
705
|
+
},
|
|
706
|
+
bounds_used: { type: 'object' },
|
|
707
|
+
layer_name: { type: 'string' },
|
|
654
708
|
context: { type: 'object' },
|
|
655
709
|
},
|
|
656
710
|
},
|
|
@@ -891,6 +945,7 @@ async function selectionChannel(connection, snippetClient, rawArgs) {
|
|
|
891
945
|
}
|
|
892
946
|
}
|
|
893
947
|
async function layerMask(connection, snippetClient, rawArgs) {
|
|
948
|
+
const { op: _omit, ...rest } = rawArgs;
|
|
894
949
|
switch (rawArgs.op) {
|
|
895
950
|
case 'create':
|
|
896
951
|
return createLayerMask(connection, snippetClient);
|
|
@@ -898,6 +953,8 @@ async function layerMask(connection, snippetClient, rawArgs) {
|
|
|
898
953
|
return deleteLayerMask(connection, snippetClient);
|
|
899
954
|
case 'apply':
|
|
900
955
|
return applyLayerMask(connection, snippetClient);
|
|
956
|
+
case 'gradient':
|
|
957
|
+
return maskGradient(connection, snippetClient, rest);
|
|
901
958
|
default:
|
|
902
959
|
return unknownDiscriminator('layer_mask op', rawArgs.op, LAYER_MASK_OPS);
|
|
903
960
|
}
|
|
@@ -1412,6 +1469,27 @@ async function applyLayerMask(connection, snippetClient) {
|
|
|
1412
1469
|
successText: () => 'Layer mask applied (merged to layer)',
|
|
1413
1470
|
});
|
|
1414
1471
|
}
|
|
1472
|
+
async function maskGradient(connection, snippetClient, rawArgs) {
|
|
1473
|
+
return runSnippetTool({
|
|
1474
|
+
connection,
|
|
1475
|
+
snippetClient,
|
|
1476
|
+
rawArgs,
|
|
1477
|
+
schema: MASK_GRADIENT_ARGS_SCHEMA,
|
|
1478
|
+
snippet: 'maskGradient',
|
|
1479
|
+
errorPrefix: 'Error drawing mask gradient',
|
|
1480
|
+
params: (args) => ({
|
|
1481
|
+
fade_to: args.fade_to ?? 'bottom',
|
|
1482
|
+
start: args.start ?? 0,
|
|
1483
|
+
end: args.end ?? 1,
|
|
1484
|
+
extent: args.extent ?? 'layer',
|
|
1485
|
+
}),
|
|
1486
|
+
successText: (result, args) => {
|
|
1487
|
+
const r = result;
|
|
1488
|
+
const created = r.created_mask ? ' (mask created)' : '';
|
|
1489
|
+
return `Mask gradient drawn: fade to ${args.fade_to ?? 'bottom'}${created}\nResult: ${JSON.stringify(result)}`;
|
|
1490
|
+
},
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1415
1493
|
async function saveSelectionToChannel(connection, snippetClient, rawArgs) {
|
|
1416
1494
|
return runSnippetTool({
|
|
1417
1495
|
connection,
|
package/dist/update/check.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { VERSION } from '../version.js';
|
|
2
2
|
import { resolveInstallChannel } from '../install-channel.js';
|
|
3
3
|
const DEFAULT_DIST_TAGS_URL = 'https://registry.npmjs.org/-/package/editmamei/dist-tags';
|
|
4
|
-
const RELEASES_URL = 'https://
|
|
4
|
+
const RELEASES_URL = 'https://editmamei.com/download';
|
|
5
5
|
export function resolveUpdateCheckUrl(env = process.env) {
|
|
6
6
|
const override = env.EDITMAMEI_UPDATE_CHECK_URL;
|
|
7
7
|
return override && override.length > 0 ? override : DEFAULT_DIST_TAGS_URL;
|