editmamei 0.20.0 → 0.22.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 +17 -1
- package/README.md +8 -7
- package/dist/api/extendscript/_helpers.js +1 -1
- 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/cli/config.js +3 -3
- package/dist/cli/install.js +2 -2
- package/dist/core/server.js +7 -7
- package/dist/core/settings.js +2 -2
- package/dist/core/tool-groups.js +91 -82
- package/dist/core/tool-tiers.js +91 -82
- package/dist/detection/landmark-detection-client.js +8 -0
- package/dist/detection/mesh-eye-correction.js +126 -0
- package/dist/detection/sam-segmenter.js +83 -0
- package/dist/index.js +2 -0
- package/dist/license/entitlement.js +52 -0
- package/dist/modules/ce/index.js +4 -4
- package/dist/perception/grounding-anchors.js +37 -0
- package/dist/perception/grounding-corners.js +93 -0
- package/dist/perception/grounding-edge-trace.js +90 -0
- package/dist/perception/grounding-extrema.js +41 -0
- package/dist/perception/grounding-gate.js +154 -0
- package/dist/perception/grounding-geometry.js +132 -0
- package/dist/perception/grounding-grid.js +34 -0
- package/dist/perception/grounding-landmarks.js +40 -0
- package/dist/perception/grounding-locate.js +341 -0
- package/dist/perception/grounding-resolver.js +202 -0
- package/dist/perception/grounding-review-crop.js +85 -0
- package/dist/perception/grounding-warp.js +182 -0
- package/dist/perception/sky-mask-transfer.js +50 -18
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/tools/adjustment-tools.js +3 -3
- package/dist/tools/brush-tools.js +111 -23
- package/dist/tools/channel-compose-tools.js +241 -0
- package/dist/tools/detection-tools.js +1 -1
- package/dist/tools/diagnostics-tools.js +1 -1
- package/dist/tools/document-tools.js +6 -6
- package/dist/tools/filter-tools.js +476 -19
- package/dist/tools/group-tools.js +10 -10
- package/dist/tools/guide-tools.js +4 -4
- package/dist/tools/history-tools.js +4 -4
- package/dist/tools/image-placement-tools.js +1 -1
- package/dist/tools/image-tools.js +62 -19
- package/dist/tools/inspect-tools.js +2 -2
- package/dist/tools/layer-ordering-tools.js +1 -1
- package/dist/tools/layer-properties-tools.js +42 -23
- package/dist/tools/layer-tools.js +10 -10
- package/dist/tools/layer-transform-tools.js +80 -41
- package/dist/tools/overview-tools.js +42 -42
- package/dist/tools/path-tools.js +53 -10
- package/dist/tools/portrait-tools.js +3 -3
- package/dist/tools/preview-tools.js +7 -82
- package/dist/tools/retouch-tools.js +93 -25
- package/dist/tools/scene-tools.js +13 -13
- package/dist/tools/selection-tools.js +319 -40
- package/dist/tools/shape-tools.js +216 -0
- package/dist/tools/text-tools.js +1 -1
- package/dist/tools/transform-canvas-tools.js +2 -2
- package/dist/tools/vector-mask-tools.js +23 -8
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/preview/coordinate-grid.js +0 -250
- package/dist/tools/object-tools.js +0 -193
- package/dist/tools/text-on-object-tools.js +0 -225
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { runScript } from '../utils/run-script.js';
|
|
2
2
|
import { validateArgs } from '../utils/validate.js';
|
|
3
|
+
import { OnnxLandmarkDetectionClient } from '../detection/landmark-detection-client.js';
|
|
4
|
+
import { resolveExpectedPlacement, PLACEMENT_SCHEMA } from '../perception/grounding-locate.js';
|
|
3
5
|
const fitLayerToDocumentSchema = {
|
|
4
6
|
type: 'object',
|
|
5
7
|
properties: {
|
|
@@ -74,6 +76,13 @@ const moveLayerSchema = {
|
|
|
74
76
|
type: 'number',
|
|
75
77
|
description: "CENTER mode: target Y for the layer's bounds CENTER point, in document pixels. Pair with center_on_x.",
|
|
76
78
|
},
|
|
79
|
+
placement: {
|
|
80
|
+
...PLACEMENT_SCHEMA,
|
|
81
|
+
description: 'ANCHOR-RELATIONAL move (preferred over guessing a pixel): a POINT relation (centroid/midpoint/offset) → ' +
|
|
82
|
+
'the layer\'s CENTER is moved to the resolved, gate-verified point (e.g. "center this layer on the detected ' +
|
|
83
|
+
'subject" / "…in the gap between the two people"). Moves ONLY if the gate PASSES. When set, delta_*/' +
|
|
84
|
+
'absolute_*/center_on_* are ignored. See ps_resolve_placement for the anchors + relation vocabulary.',
|
|
85
|
+
},
|
|
77
86
|
},
|
|
78
87
|
};
|
|
79
88
|
const rotateLayerSchema = {
|
|
@@ -157,12 +166,12 @@ const TRANSFORM_INPUT_SCHEMA = {
|
|
|
157
166
|
},
|
|
158
167
|
required: ['op'],
|
|
159
168
|
};
|
|
160
|
-
export function createLayerTransformTools(connection, snippetClient) {
|
|
169
|
+
export function createLayerTransformTools(connection, snippetClient, client = new OnnxLandmarkDetectionClient()) {
|
|
161
170
|
return [
|
|
162
171
|
{
|
|
163
172
|
tool: {
|
|
164
|
-
name: '
|
|
165
|
-
description: 'Transform the active layer — choose the operation with `op`. `fit` scales to fit (letterbox) or fill (crop) the canvas and centers (idempotent). `scale` does uniform (scale_percent) or non-uniform stretch/squash (scale_x_percent/scale_y_percent) — multiplicative, not idempotent. `move` translates via exactly one of delta (relative), absolute (top-left target),
|
|
173
|
+
name: 'ps_transform_layer',
|
|
174
|
+
description: 'Transform the active layer — choose the operation with `op`. `fit` scales to fit (letterbox) or fill (crop) the canvas and centers (idempotent). `scale` does uniform (scale_percent) or non-uniform stretch/squash (scale_x_percent/scale_y_percent) — multiplicative, not idempotent. `move` translates via exactly one of delta (relative), absolute (top-left target), center_on (center target — best for "place inside the frame opening"), or an anchor-relational `placement` (preferred: a point relation → the layer center moves to the resolved, gate-verified point); mixing the raw modes errors. `rotate` rotates by relative degrees around center. `flip` mirrors horizontal/vertical. `skew` slants the layer (skew_h_degrees/skew_v_degrees). `free` is a numeric free-transform (scale + degrees + offset). scale/move/rotate/flip/skew/free auto-promote the background layer (background_promoted=true).',
|
|
166
175
|
inputSchema: TRANSFORM_INPUT_SCHEMA,
|
|
167
176
|
outputSchema: {
|
|
168
177
|
type: 'object',
|
|
@@ -190,6 +199,16 @@ export function createLayerTransformTools(connection, snippetClient) {
|
|
|
190
199
|
applied_delta_y: { type: 'number' },
|
|
191
200
|
new_bounds: { type: 'object' },
|
|
192
201
|
background_promoted: { type: 'boolean' },
|
|
202
|
+
placement: {
|
|
203
|
+
type: 'object',
|
|
204
|
+
description: 'Present when op=move used anchor-relational placement: the resolved point + gate verdict.',
|
|
205
|
+
properties: {
|
|
206
|
+
target: { type: 'string' },
|
|
207
|
+
gate: { type: 'object' },
|
|
208
|
+
anchors: { type: 'object' },
|
|
209
|
+
geometry: { type: 'object' },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
193
212
|
context: { type: 'object' },
|
|
194
213
|
},
|
|
195
214
|
},
|
|
@@ -198,11 +217,11 @@ export function createLayerTransformTools(connection, snippetClient) {
|
|
|
198
217
|
idempotentHint: false,
|
|
199
218
|
},
|
|
200
219
|
},
|
|
201
|
-
handler: async (args) => transformLayer(connection, snippetClient, args),
|
|
220
|
+
handler: async (args) => transformLayer(connection, snippetClient, client, args),
|
|
202
221
|
},
|
|
203
222
|
];
|
|
204
223
|
}
|
|
205
|
-
async function transformLayer(connection, snippetClient, rawArgs) {
|
|
224
|
+
async function transformLayer(connection, snippetClient, client, rawArgs) {
|
|
206
225
|
const op = rawArgs.op;
|
|
207
226
|
const { op: _omit, ...rest } = rawArgs;
|
|
208
227
|
switch (op) {
|
|
@@ -211,7 +230,7 @@ async function transformLayer(connection, snippetClient, rawArgs) {
|
|
|
211
230
|
case 'scale':
|
|
212
231
|
return scaleLayer(connection, snippetClient, rest);
|
|
213
232
|
case 'move':
|
|
214
|
-
return moveLayer(connection, snippetClient, rest);
|
|
233
|
+
return moveLayer(connection, snippetClient, client, rest);
|
|
215
234
|
case 'rotate':
|
|
216
235
|
return rotateLayer(connection, snippetClient, rest);
|
|
217
236
|
case 'flip':
|
|
@@ -338,39 +357,47 @@ async function flipLayer(connection, snippetClient, rawArgs) {
|
|
|
338
357
|
};
|
|
339
358
|
}
|
|
340
359
|
}
|
|
341
|
-
async function moveLayer(connection, snippetClient, rawArgs) {
|
|
360
|
+
async function moveLayer(connection, snippetClient, client, rawArgs) {
|
|
342
361
|
try {
|
|
343
362
|
const args = validateArgs(moveLayerSchema, rawArgs);
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
if (hasDelta && (args.delta_x === undefined || args.delta_y === undefined)) {
|
|
355
|
-
throw new Error('Delta mode requires both delta_x and delta_y.');
|
|
363
|
+
let mode;
|
|
364
|
+
let deltaX = 0, deltaY = 0, absoluteX = 0, absoluteY = 0, centerOnX = 0, centerOnY = 0;
|
|
365
|
+
let placementInfo;
|
|
366
|
+
if (args.placement) {
|
|
367
|
+
const rp = await resolveExpectedPlacement(connection, client, args.placement, 'point', 'move');
|
|
368
|
+
mode = 'center';
|
|
369
|
+
centerOnX = rp.point.x;
|
|
370
|
+
centerOnY = rp.point.y;
|
|
371
|
+
placementInfo = { target: rp.target, anchors: rp.anchors, point: rp.point };
|
|
356
372
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
373
|
+
else {
|
|
374
|
+
const hasDelta = args.delta_x !== undefined || args.delta_y !== undefined;
|
|
375
|
+
const hasAbsolute = args.absolute_x !== undefined || args.absolute_y !== undefined;
|
|
376
|
+
const hasCenter = args.center_on_x !== undefined || args.center_on_y !== undefined;
|
|
377
|
+
const modeCount = (hasDelta ? 1 : 0) + (hasAbsolute ? 1 : 0) + (hasCenter ? 1 : 0);
|
|
378
|
+
if (modeCount === 0) {
|
|
379
|
+
throw new Error('No positioning mode specified. Pass a placement (anchors + a point relation), or exactly one pair: delta_x+delta_y (relative), absolute_x+absolute_y (target top-left), or center_on_x+center_on_y (target center).');
|
|
380
|
+
}
|
|
381
|
+
if (modeCount > 1) {
|
|
382
|
+
throw new Error('Mixed positioning modes. Pass exactly ONE pair: delta_x+delta_y OR absolute_x+absolute_y OR center_on_x+center_on_y.');
|
|
383
|
+
}
|
|
384
|
+
if (hasDelta && (args.delta_x === undefined || args.delta_y === undefined)) {
|
|
385
|
+
throw new Error('Delta mode requires both delta_x and delta_y.');
|
|
386
|
+
}
|
|
387
|
+
if (hasAbsolute && (args.absolute_x === undefined || args.absolute_y === undefined)) {
|
|
388
|
+
throw new Error('Absolute mode requires both absolute_x and absolute_y.');
|
|
389
|
+
}
|
|
390
|
+
if (hasCenter && (args.center_on_x === undefined || args.center_on_y === undefined)) {
|
|
391
|
+
throw new Error('Center mode requires both center_on_x and center_on_y.');
|
|
392
|
+
}
|
|
393
|
+
mode = hasAbsolute ? 'absolute' : hasCenter ? 'center' : 'delta';
|
|
394
|
+
deltaX = args.delta_x ?? 0;
|
|
395
|
+
deltaY = args.delta_y ?? 0;
|
|
396
|
+
absoluteX = args.absolute_x ?? 0;
|
|
397
|
+
absoluteY = args.absolute_y ?? 0;
|
|
398
|
+
centerOnX = args.center_on_x ?? 0;
|
|
399
|
+
centerOnY = args.center_on_y ?? 0;
|
|
362
400
|
}
|
|
363
|
-
const mode = hasAbsolute
|
|
364
|
-
? 'absolute'
|
|
365
|
-
: hasCenter
|
|
366
|
-
? 'center'
|
|
367
|
-
: 'delta';
|
|
368
|
-
const deltaX = args.delta_x ?? 0;
|
|
369
|
-
const deltaY = args.delta_y ?? 0;
|
|
370
|
-
const absoluteX = args.absolute_x ?? 0;
|
|
371
|
-
const absoluteY = args.absolute_y ?? 0;
|
|
372
|
-
const centerOnX = args.center_on_x ?? 0;
|
|
373
|
-
const centerOnY = args.center_on_y ?? 0;
|
|
374
401
|
const script = await snippetClient.build('moveLayer', {
|
|
375
402
|
deltaX,
|
|
376
403
|
deltaY,
|
|
@@ -381,11 +408,13 @@ async function moveLayer(connection, snippetClient, rawArgs) {
|
|
|
381
408
|
centerOnY,
|
|
382
409
|
});
|
|
383
410
|
const result = (await runScript(connection, script));
|
|
384
|
-
const summary =
|
|
385
|
-
? `Layer moved
|
|
386
|
-
: mode === '
|
|
387
|
-
? `Layer
|
|
388
|
-
:
|
|
411
|
+
const summary = placementInfo
|
|
412
|
+
? `Layer center moved to (${centerOnX}, ${centerOnY})px via placement (gate PASS) — applied delta (${result.applied_delta_x ?? '?'}, ${result.applied_delta_y ?? '?'})`
|
|
413
|
+
: mode === 'delta'
|
|
414
|
+
? `Layer moved by (${deltaX}, ${deltaY})px (relative)`
|
|
415
|
+
: mode === 'absolute'
|
|
416
|
+
? `Layer top-left moved to (${absoluteX}, ${absoluteY})px — applied delta (${result.applied_delta_x ?? '?'}, ${result.applied_delta_y ?? '?'})`
|
|
417
|
+
: `Layer center moved to (${centerOnX}, ${centerOnY})px — applied delta (${result.applied_delta_x ?? '?'}, ${result.applied_delta_y ?? '?'})`;
|
|
389
418
|
const bgNote = result.background_promoted ? ' (background layer auto-promoted)' : '';
|
|
390
419
|
return {
|
|
391
420
|
content: [
|
|
@@ -394,7 +423,17 @@ async function moveLayer(connection, snippetClient, rawArgs) {
|
|
|
394
423
|
text: `${summary}${bgNote}\nResult: ${JSON.stringify(result)}`,
|
|
395
424
|
},
|
|
396
425
|
],
|
|
397
|
-
structuredContent:
|
|
426
|
+
structuredContent: placementInfo
|
|
427
|
+
? {
|
|
428
|
+
...result,
|
|
429
|
+
placement: {
|
|
430
|
+
target: placementInfo.target,
|
|
431
|
+
gate: { pass: true },
|
|
432
|
+
anchors: placementInfo.anchors,
|
|
433
|
+
geometry: placementInfo.point,
|
|
434
|
+
},
|
|
435
|
+
}
|
|
436
|
+
: result,
|
|
398
437
|
};
|
|
399
438
|
}
|
|
400
439
|
catch (error) {
|
|
@@ -9,26 +9,26 @@ already know which tool fits.
|
|
|
9
9
|
|
|
10
10
|
If you reference a tool by name that isn't in your \`tools/list\`,
|
|
11
11
|
work around it — don't speculate that it's there. The discovery chain
|
|
12
|
-
is \`
|
|
13
|
-
\`
|
|
12
|
+
is \`ps_ping\` (liveness) → this overview (workflow) →
|
|
13
|
+
\`ps_list_capabilities\` (a compact, live map of what exists,
|
|
14
14
|
grouped by capability) → \`tools/list\` (full schemas).
|
|
15
15
|
|
|
16
16
|
## The workflow contract
|
|
17
17
|
|
|
18
18
|
Every non-trivial edit follows the same six phases:
|
|
19
19
|
|
|
20
|
-
1. **Assess** — \`
|
|
21
|
-
selection_info) + \`
|
|
20
|
+
1. **Assess** — \`ps_inspect\` (what=metadata / layer_tree / history /
|
|
21
|
+
selection_info) + \`ps_get_preview\` to understand what you're working with.
|
|
22
22
|
2. **Plan** — name the intent, the layers/regions involved, and the
|
|
23
23
|
exit criteria. Tell the user before you execute.
|
|
24
24
|
3. **Enact** — call the tools.
|
|
25
|
-
4. **Check** — \`
|
|
25
|
+
4. **Check** — \`ps_get_preview\` after every load-bearing change.
|
|
26
26
|
For spatial questions ("is this aligned?"), use annotations or
|
|
27
|
-
\`
|
|
27
|
+
\`ps_get_layer_bounds_diff\` — see Verification below.
|
|
28
28
|
5. **Iterate** — up to 3 correction loops. If the third attempt still
|
|
29
29
|
misses, surface the gap to the user instead of looping further.
|
|
30
|
-
6. **Finish** — \`
|
|
31
|
-
\`
|
|
30
|
+
6. **Finish** — \`ps_export\` (format jpeg/png) /
|
|
31
|
+
\`ps_save_psd\`. Confirm the user wants to flatten before
|
|
32
32
|
destructive saves.
|
|
33
33
|
|
|
34
34
|
## Capabilities map
|
|
@@ -36,61 +36,61 @@ Every non-trivial edit follows the same six phases:
|
|
|
36
36
|
This map covers the workflow categories. For specific tool names and
|
|
37
37
|
schemas in this session, consult \`tools/list\`.
|
|
38
38
|
|
|
39
|
-
- **Adjustments (non-destructive)** — \`
|
|
39
|
+
- **Adjustments (non-destructive)** — \`ps_add_adjustment_layer\`
|
|
40
40
|
covers brightness/contrast, levels, curves, hue/sat, color balance,
|
|
41
41
|
photo filter, vibrance, channel mixer, selective color, black & white,
|
|
42
42
|
gradient map, exposure, color lookup, invert. Always prefer this over
|
|
43
43
|
destructive filters for tone/color work.
|
|
44
|
-
- **Filters (destructive)** — \`
|
|
44
|
+
- **Filters (destructive)** — \`ps_apply_filter\` with a
|
|
45
45
|
\`type\` (gaussian_blur, motion_blur, lens_blur, radial_blur, sharpen,
|
|
46
46
|
smart_sharpen, noise, reduce_noise, high_pass, pixelate, distort,
|
|
47
47
|
displace, oil_paint). Auto-duplicates the active layer by default so
|
|
48
48
|
the original is preserved (\`target_was_copy: true\` in the response);
|
|
49
49
|
pass \`apply_to_active_layer: true\` only when the user explicitly
|
|
50
50
|
wants to bake into the original. Destructive tonal bakes with no
|
|
51
|
-
adjustment-layer equivalent live in \`
|
|
51
|
+
adjustment-layer equivalent live in \`ps_apply_adjustment\`
|
|
52
52
|
(type shadows_highlights / equalize / color_lookup).
|
|
53
53
|
- **Layers** — create / delete / duplicate / select / rasterize /
|
|
54
|
-
convert-to-smart-object / add layer style, plus \`
|
|
54
|
+
convert-to-smart-object / add layer style, plus \`ps_set_layer\`
|
|
55
55
|
(property opacity / blend_mode / visibility / locked / name),
|
|
56
|
-
\`
|
|
57
|
-
\`
|
|
56
|
+
\`ps_merge\` (mode visible / stamp / flatten), and
|
|
57
|
+
\`ps_transform_layer\` (op move / rotate / scale / fit / flip —
|
|
58
58
|
auto-promotes the background layer, so you don't have to duplicate
|
|
59
59
|
first).
|
|
60
|
-
- **Masks** — \`
|
|
60
|
+
- **Masks** — \`ps_layer_mask\` (op create / delete / apply).
|
|
61
61
|
\`op: "create"\` automatically uses the current selection if one is
|
|
62
62
|
active (reveals selection, hides outside); with no selection it
|
|
63
63
|
creates a reveal-all mask. This is the right tool for "mask the placed
|
|
64
64
|
image to the frame opening" — make the selection first, then call it.
|
|
65
65
|
Don't write \`Mk Chnl At=Msk\` AM scripts for this — it's a one-call
|
|
66
|
-
native. For VECTOR masks + work paths, see \`
|
|
67
|
-
(op add / delete / link / unlink) and \`
|
|
66
|
+
native. For VECTOR masks + work paths, see \`ps_vector_mask\`
|
|
67
|
+
(op add / delete / link / unlink) and \`ps_path\`
|
|
68
68
|
(selection↔path round-trip, stroke / fill / clip).
|
|
69
|
-
- **Selection** — \`
|
|
69
|
+
- **Selection** — \`ps_select\` (mode rectangle / color_range /
|
|
70
70
|
luminance_range / magic_wand / all / none / inverse),
|
|
71
|
-
\`
|
|
72
|
-
\`
|
|
73
|
-
\`
|
|
71
|
+
\`ps_modify_selection\` (op feather / refine_edge),
|
|
72
|
+
\`ps_selection_channel\` (op save / load),
|
|
73
|
+
\`ps_inspect\` (what=selection_info), \`ps_get_selection_preview\`.
|
|
74
74
|
Smart-selection (AI subject / sky) helpers may be available in
|
|
75
75
|
\`tools/list\` — check there.
|
|
76
|
-
- **Perception (on-device CV)** — \`
|
|
77
|
-
COCO-80 objects → real document-pixel boxes), \`
|
|
76
|
+
- **Perception (on-device CV)** — \`ps_detect\` (faces +
|
|
77
|
+
COCO-80 objects → real document-pixel boxes), \`ps_read_scene\`
|
|
78
78
|
(a structured read of subjects / horizon / tonal zones), and
|
|
79
|
-
\`
|
|
79
|
+
\`ps_select_by_reference\` (a NAMED target → a real pixel
|
|
80
80
|
selection instead of a guessed rectangle). The image is analyzed
|
|
81
81
|
locally. Reach for these to get REAL coordinates rather than
|
|
82
82
|
estimating from the preview — they're the antidote to spatial
|
|
83
83
|
guessing.
|
|
84
|
-
- **Text** — \`
|
|
84
|
+
- **Text** — \`ps_create_text_layer\`, then \`ps_set_text\`
|
|
85
85
|
(property font / color / alignment / content; font resolves family
|
|
86
86
|
names to PostScript).
|
|
87
|
-
- **Image** — \`
|
|
88
|
-
objects), \`
|
|
89
|
-
\`
|
|
90
|
-
\`
|
|
91
|
-
- **Groups** — \`
|
|
92
|
-
\`
|
|
93
|
-
\`
|
|
87
|
+
- **Image** — \`ps_place_image\` (links external files as smart
|
|
88
|
+
objects), \`ps_resize_image\`, \`ps_crop_document\`,
|
|
89
|
+
\`ps_convert_image_mode\`, open / close / save_psd, and
|
|
90
|
+
\`ps_export\` (format jpeg/png).
|
|
91
|
+
- **Groups** — \`ps_create_group\`,
|
|
92
|
+
\`ps_move_layer_to_group\`, \`ps_set_group_blend_mode\`,
|
|
93
|
+
\`ps_ungroup\`, \`ps_delete_group\`.
|
|
94
94
|
- **Verification** — see below; this is the most underused part of
|
|
95
95
|
the surface.
|
|
96
96
|
- **Templates** — when template tools are present in \`tools/list\`,
|
|
@@ -107,20 +107,20 @@ Vision-language models are bad at fine spatial estimation. You will
|
|
|
107
107
|
hallucinate "looks good" when reviewing your own output. The fix is
|
|
108
108
|
to convert spatial questions into semantic ones:
|
|
109
109
|
|
|
110
|
-
- **\`
|
|
110
|
+
- **\`ps_get_preview\`** with the \`annotations\` array — draw
|
|
111
111
|
rectangles (by explicit bounds OR by layer name), guides, points,
|
|
112
112
|
or selection markers ON the preview. "Is the placed image aligned
|
|
113
113
|
with the frame?" becomes "Is the red wireframe touching the frame
|
|
114
114
|
edges?" — a question you can answer reliably.
|
|
115
|
-
- **\`
|
|
115
|
+
- **\`ps_get_layer_bounds_diff\`** — pass a target rectangle
|
|
116
116
|
+ a layer name + a tolerance. Returns a one-word verdict
|
|
117
117
|
("aligned" / "shifted right" / "layer too small") and numeric
|
|
118
118
|
deltas. Use this when you need a yes/no on placement accuracy,
|
|
119
119
|
not a visual judgment.
|
|
120
|
-
- **\`
|
|
120
|
+
- **\`ps_compare_regions\`** — per-channel histogram stats
|
|
121
121
|
for two rectangles. Reach for this when you need a numeric diff
|
|
122
122
|
(matched tones across two crops, before/after for an adjustment).
|
|
123
|
-
- **\`
|
|
123
|
+
- **\`ps_get_histogram\`** — full-image or per-channel. Reach
|
|
124
124
|
for this for clipping detection, exposure verification, or "is
|
|
125
125
|
this image neutral-gray?" — \`get_preview\` + eyeballing doesn't
|
|
126
126
|
surface clipping reliably.
|
|
@@ -134,12 +134,12 @@ fill black, for example. Re-read your inventory first.
|
|
|
134
134
|
|
|
135
135
|
Common false escape-hatch reaches we want you to avoid:
|
|
136
136
|
- "Add a layer mask from the current selection" →
|
|
137
|
-
\`
|
|
137
|
+
\`ps_layer_mask\` (op create) does this automatically when a
|
|
138
138
|
selection is live.
|
|
139
139
|
- "Move the layer so its center is at (x, y)" →
|
|
140
|
-
\`
|
|
140
|
+
\`ps_transform_layer\` (op move) with \`center_on_x\` /
|
|
141
141
|
\`center_on_y\`.
|
|
142
|
-
- "Rotate this background layer" → \`
|
|
142
|
+
- "Rotate this background layer" → \`ps_transform_layer\`
|
|
143
143
|
(op rotate); it auto-promotes the background layer.
|
|
144
144
|
|
|
145
145
|
If no typed tool fits AND the operation isn't a composition, surface
|
|
@@ -148,7 +148,7 @@ the gap to the user rather than fabricating a workaround.
|
|
|
148
148
|
## Known gaps
|
|
149
149
|
|
|
150
150
|
- Frame opening / arbitrary edge detection — no general primitive.
|
|
151
|
-
Best path: \`
|
|
151
|
+
Best path: \`ps_select\` (mode magic_wand) inside the dark
|
|
152
152
|
interior + read the selection bounds via \`get_selection_info\`.
|
|
153
153
|
- Smart Object editing — no native tool to enter / exit smart-object
|
|
154
154
|
edit mode from outside the document. Plan around it.
|
|
@@ -159,7 +159,7 @@ the gap to the user rather than fabricating a workaround.
|
|
|
159
159
|
|
|
160
160
|
If you've lost track of what's available — this brief scrolled out of
|
|
161
161
|
context, or you're not sure which tool exists for a job — call
|
|
162
|
-
\`
|
|
162
|
+
\`ps_list_capabilities\`. It returns a compact, live map of every
|
|
163
163
|
tool grouped by capability (each group's purpose + the tool names in
|
|
164
164
|
it). It's cheaper than re-reading this overview and more current than
|
|
165
165
|
any static list. Use it to find the right tool name, then \`tools/list\`
|
|
@@ -179,7 +179,7 @@ export function createOverviewTools(_connection, _snippetClient) {
|
|
|
179
179
|
return [
|
|
180
180
|
{
|
|
181
181
|
tool: {
|
|
182
|
-
name: '
|
|
182
|
+
name: 'ps_overview',
|
|
183
183
|
description: 'Orientation brief for the entire MCP — workflow contract, capabilities map by category, verification primitives, escape-hatch policy, known gaps. READ THIS FIRST when the user gives you an open-ended editing task. Read-only, idempotent, no document required, no Photoshop call (returns a static markdown brief). Skip it for trivial single-tool requests where you already know which tool fits.',
|
|
184
184
|
inputSchema: overviewSchema,
|
|
185
185
|
outputSchema: {
|
package/dist/tools/path-tools.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { SUPPORTED_BRUSH_TOOLS } from '../api/brush-tool-names.js';
|
|
2
2
|
import { runScript } from '../utils/run-script.js';
|
|
3
3
|
import { validateArgs } from '../utils/validate.js';
|
|
4
|
+
import { OnnxLandmarkDetectionClient } from '../detection/landmark-detection-client.js';
|
|
5
|
+
import { resolveExpectedPlacement, PLACEMENT_SCHEMA } from '../perception/grounding-locate.js';
|
|
4
6
|
const PATH_OPS = [
|
|
5
7
|
'create_from_selection',
|
|
8
|
+
'create_from_placement',
|
|
6
9
|
'save',
|
|
7
10
|
'list',
|
|
8
11
|
'delete',
|
|
@@ -20,6 +23,7 @@ const pathInputSchema = {
|
|
|
20
23
|
type: 'string',
|
|
21
24
|
enum: [...PATH_OPS],
|
|
22
25
|
description: 'create_from_selection: convert the active selection into a work path (tolerance = crude↔detailed dial; CLEARS the selection). ' +
|
|
26
|
+
'create_from_placement: NAME a curve (placement → a path relation: `along` a traced edge / landmark curve, or a `segment` between anchors) and get an editable, saved vector path following it — the grounded pen; needs `name` (+ optional `closed`). ' +
|
|
23
27
|
'save: persist the current work path under a name (required for stroke/clip-by-name and to survive the next create). ' +
|
|
24
28
|
'list: inventory all paths (name, kind, subpath + anchor counts) — read-only. ' +
|
|
25
29
|
'delete: remove a path (name → that saved path; omit name → the current work path). ' +
|
|
@@ -30,7 +34,16 @@ const pathInputSchema = {
|
|
|
30
34
|
},
|
|
31
35
|
name: {
|
|
32
36
|
type: 'string',
|
|
33
|
-
description: 'Path name. REQUIRED for save and
|
|
37
|
+
description: 'Path name. REQUIRED for save, set_clipping, and create_from_placement (the new saved path is named). Optional for delete / load_as_selection / stroke / fill — when omitted those target the current work path. Ignored by create_from_selection / list.',
|
|
38
|
+
},
|
|
39
|
+
placement: {
|
|
40
|
+
...PLACEMENT_SCHEMA,
|
|
41
|
+
description: 'create_from_placement only: NAME the curve the path follows, as an anchor-relational placement that resolves to a PATH (relation `along` a traced edge / a Pro landmark curve, or a `segment` between two anchors). The spatial-grounding resolver + objective gate produce AND verify the curve; the path is created ONLY if the gate PASSES. Name the curve — do NOT hand-type anchor points.',
|
|
42
|
+
},
|
|
43
|
+
closed: {
|
|
44
|
+
type: 'boolean',
|
|
45
|
+
default: false,
|
|
46
|
+
description: 'create_from_placement only: close the path into a loop (connect the last anchor back to the first). Default false (an open path).',
|
|
34
47
|
},
|
|
35
48
|
tolerance: {
|
|
36
49
|
type: 'number',
|
|
@@ -59,7 +72,7 @@ const pathInputSchema = {
|
|
|
59
72
|
tool: {
|
|
60
73
|
type: 'string',
|
|
61
74
|
enum: SUPPORTED_BRUSH_TOOLS,
|
|
62
|
-
description: "stroke only: which brush-family tool paints the path. Default 'brush'. Same 16-tool set as
|
|
75
|
+
description: "stroke only: which brush-family tool paints the path. Default 'brush'. Same 16-tool set as ps_apply_brush_stroke.",
|
|
63
76
|
default: 'brush',
|
|
64
77
|
},
|
|
65
78
|
color: {
|
|
@@ -98,12 +111,12 @@ const pathInputSchema = {
|
|
|
98
111
|
},
|
|
99
112
|
required: ['op'],
|
|
100
113
|
};
|
|
101
|
-
export function createPathTools(connection, snippetClient) {
|
|
114
|
+
export function createPathTools(connection, snippetClient, client = new OnnxLandmarkDetectionClient()) {
|
|
102
115
|
return [
|
|
103
116
|
{
|
|
104
117
|
tool: {
|
|
105
|
-
name: '
|
|
106
|
-
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), `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**: 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 (Bundle O). 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.',
|
|
118
|
+
name: 'ps_path',
|
|
119
|
+
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 (Bundle O). 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.',
|
|
107
120
|
inputSchema: pathInputSchema,
|
|
108
121
|
outputSchema: {
|
|
109
122
|
type: 'object',
|
|
@@ -118,6 +131,9 @@ export function createPathTools(connection, snippetClient) {
|
|
|
118
131
|
name: { type: 'string' },
|
|
119
132
|
path_name: { type: 'string' },
|
|
120
133
|
tolerance: { type: 'number' },
|
|
134
|
+
anchors: { type: 'number' },
|
|
135
|
+
closed: { type: 'boolean' },
|
|
136
|
+
placement: { type: 'object' },
|
|
121
137
|
selection_consumed: { type: 'boolean' },
|
|
122
138
|
operation: { type: 'string' },
|
|
123
139
|
tool: { type: 'string' },
|
|
@@ -139,22 +155,32 @@ export function createPathTools(connection, snippetClient) {
|
|
|
139
155
|
idempotentHint: false,
|
|
140
156
|
},
|
|
141
157
|
},
|
|
142
|
-
handler: async (args) => runPathOp(connection, snippetClient, args),
|
|
158
|
+
handler: async (args) => runPathOp(connection, snippetClient, client, args),
|
|
143
159
|
},
|
|
144
160
|
];
|
|
145
161
|
}
|
|
146
|
-
async function runPathOp(connection, snippetClient, rawArgs) {
|
|
162
|
+
async function runPathOp(connection, snippetClient, client, rawArgs) {
|
|
147
163
|
try {
|
|
148
164
|
const args = validateArgs(pathInputSchema, rawArgs);
|
|
149
165
|
const op = args.op;
|
|
150
166
|
const name = args.name;
|
|
151
167
|
let snippet;
|
|
152
168
|
let params;
|
|
169
|
+
let placementInfo;
|
|
153
170
|
switch (op) {
|
|
154
171
|
case 'create_from_selection':
|
|
155
172
|
snippet = 'createPathFromSelection';
|
|
156
173
|
params = { tolerance: args.tolerance };
|
|
157
174
|
break;
|
|
175
|
+
case 'create_from_placement': {
|
|
176
|
+
if (!name)
|
|
177
|
+
return errorResult('create_from_placement requires a "name" for the new saved path (pathItems.add needs one).');
|
|
178
|
+
const rp = await resolveExpectedPlacement(connection, client, args.placement, 'path', 'path');
|
|
179
|
+
snippet = 'createPathFromPoints';
|
|
180
|
+
params = { name, points: rp.curve, closed: args.closed ?? false };
|
|
181
|
+
placementInfo = { curve: rp.curve, anchors: rp.anchors, summary: rp.summary };
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
158
184
|
case 'save':
|
|
159
185
|
if (!name)
|
|
160
186
|
return missingName('save');
|
|
@@ -219,8 +245,20 @@ async function runPathOp(connection, snippetClient, rawArgs) {
|
|
|
219
245
|
}
|
|
220
246
|
const script = await snippetClient.build(snippet, params);
|
|
221
247
|
const result = (await runScript(connection, script));
|
|
248
|
+
if (placementInfo) {
|
|
249
|
+
result.placement = {
|
|
250
|
+
target: 'path',
|
|
251
|
+
gate: { pass: true },
|
|
252
|
+
anchors: placementInfo.anchors,
|
|
253
|
+
points: placementInfo.curve.length,
|
|
254
|
+
summary: placementInfo.summary,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const text = placementInfo
|
|
258
|
+
? `${summarize(op, result)} Path follows the resolved ${placementInfo.summary} (placement gate PASS).`
|
|
259
|
+
: summarize(op, result);
|
|
222
260
|
return {
|
|
223
|
-
content: [{ type: 'text', text
|
|
261
|
+
content: [{ type: 'text', text }],
|
|
224
262
|
structuredContent: result,
|
|
225
263
|
};
|
|
226
264
|
}
|
|
@@ -229,7 +267,7 @@ async function runPathOp(connection, snippetClient, rawArgs) {
|
|
|
229
267
|
content: [
|
|
230
268
|
{
|
|
231
269
|
type: 'text',
|
|
232
|
-
text: `Error in
|
|
270
|
+
text: `Error in ps_path: ${error instanceof Error ? error.message : String(error)}`,
|
|
233
271
|
},
|
|
234
272
|
],
|
|
235
273
|
isError: true,
|
|
@@ -241,12 +279,15 @@ function missingName(op) {
|
|
|
241
279
|
content: [
|
|
242
280
|
{
|
|
243
281
|
type: 'text',
|
|
244
|
-
text: `
|
|
282
|
+
text: `ps_path op=${op} requires a "name" (the saved path to act on). Use op=save to name the current work path first.`,
|
|
245
283
|
},
|
|
246
284
|
],
|
|
247
285
|
isError: true,
|
|
248
286
|
};
|
|
249
287
|
}
|
|
288
|
+
function errorResult(msg) {
|
|
289
|
+
return { content: [{ type: 'text', text: `ps_path: ${msg}` }], isError: true };
|
|
290
|
+
}
|
|
250
291
|
function summarize(op, r) {
|
|
251
292
|
const info = r.path_info;
|
|
252
293
|
const pathCount = info?.count ?? r.count;
|
|
@@ -254,6 +295,8 @@ function summarize(op, r) {
|
|
|
254
295
|
switch (op) {
|
|
255
296
|
case 'create_from_selection':
|
|
256
297
|
return `Created a work path from the selection (tolerance ${r.tolerance}); the selection was consumed.${suffix}`;
|
|
298
|
+
case 'create_from_placement':
|
|
299
|
+
return `Created editable path "${r.name}" from the named curve (${r.anchors} anchors${r.closed ? ', closed' : ''}).${suffix}`;
|
|
257
300
|
case 'save':
|
|
258
301
|
return `Saved path "${r.name}".${suffix}`;
|
|
259
302
|
case 'list':
|
|
@@ -112,7 +112,7 @@ async function runPortraitOp(connection, snippet, client, rawArgs) {
|
|
|
112
112
|
catch (error) {
|
|
113
113
|
const msg = error instanceof Error ? error.message : String(error);
|
|
114
114
|
return {
|
|
115
|
-
content: [{ type: 'text', text: `Error in
|
|
115
|
+
content: [{ type: 'text', text: `Error in ps_portrait_touchup: ${msg}` }],
|
|
116
116
|
isError: true,
|
|
117
117
|
};
|
|
118
118
|
}
|
|
@@ -121,8 +121,8 @@ export function createPortraitTools(connection, snippetClient, client = new Onnx
|
|
|
121
121
|
return [
|
|
122
122
|
{
|
|
123
123
|
tool: {
|
|
124
|
-
name: '
|
|
125
|
-
description: 'Detection-driven face touch-ups using LOCAL on-device face detection. op=dodge_face brightens the detected face(s) with a non-destructive masked adjustment layer. op=soften_skin applies a soft blur over the face(s) on a duplicate layer (light, bounding-box-level smoothing). Works from the face bounding box — precise landmark ops (eyes/teeth/lips) are a Pro feature. Multiple faces are handled together. Pair with
|
|
124
|
+
name: 'ps_portrait_touchup',
|
|
125
|
+
description: 'Detection-driven face touch-ups using LOCAL on-device face detection. op=dodge_face brightens the detected face(s) with a non-destructive masked adjustment layer. op=soften_skin applies a soft blur over the face(s) on a duplicate layer (light, bounding-box-level smoothing). Works from the face bounding box — precise landmark ops (eyes/teeth/lips) are a Pro feature. Multiple faces are handled together. Pair with ps_detect (target=faces) to preview detection first.',
|
|
126
126
|
inputSchema: portraitSchema,
|
|
127
127
|
outputSchema: {
|
|
128
128
|
type: 'object',
|