editmamei 1.3.0 → 1.4.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/README.md +6 -5
- 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 +3 -3
- package/dist/core/tool-groups.js +3 -3
- package/dist/core/tool-registry.js +3 -1
- package/dist/core/tool-tiers.js +1 -1
- package/dist/kernel/kernel.js +1 -0
- package/dist/modules/ce/index.js +7 -0
- package/dist/perception/facets.js +4 -3
- package/dist/perception/region-scorer.js +1 -1
- package/dist/perception/scene-model.js +1 -1
- package/dist/perception/select-recipes.js +14 -3
- package/dist/platform/launch-readiness.js +25 -0
- package/dist/platform/macos-runner.js +18 -8
- package/dist/platform/script-queue.js +5 -19
- package/dist/platform/windows-runner.js +18 -8
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/telemetry/client.js +11 -4
- package/dist/telemetry/events.js +5 -2
- package/dist/tools/detection-tools.js +4 -4
- package/dist/tools/preview-tools.js +1 -1
- package/dist/tools/scene-tools.js +8 -6
- package/dist/tools/selection-tools.js +155 -109
- package/dist/tools/sequence-tools.js +435 -0
- package/dist/utils/operation-timeouts.js +92 -0
- package/dist/utils/run-script.js +25 -1
- package/dist/utils/session-log.js +15 -1
- package/dist/utils/tool-budget-context.js +14 -0
- package/dist/version.js +1 -1
- package/package.json +8 -2
|
@@ -316,6 +316,24 @@ const selectPolygonSchema = {
|
|
|
316
316
|
},
|
|
317
317
|
required: ['points'],
|
|
318
318
|
};
|
|
319
|
+
const selectFocusAreaSchema = {
|
|
320
|
+
type: 'object',
|
|
321
|
+
properties: {
|
|
322
|
+
in_focus_radius: {
|
|
323
|
+
type: 'number',
|
|
324
|
+
description: 'How much blur still counts as "in focus", in pixels. Higher pulls more of the soft transition zone into the selection; lower keeps only the crisply resolved plane. 4.07 is the Photoshop dialog default and a sane starting point. The useful band is narrow, and a radius well above the default selects the entire frame — so move in small steps and CHECK the returned area_percent and whole_canvas_selected: a selection covering essentially everything means the radius is too high and the result is worthless, even though the call reports success.',
|
|
325
|
+
default: 4.07,
|
|
326
|
+
minimum: 0.1,
|
|
327
|
+
maximum: 15,
|
|
328
|
+
},
|
|
329
|
+
soft_mask: {
|
|
330
|
+
type: 'boolean',
|
|
331
|
+
description: 'False (default) yields a hard-edged selection — every pixel fully in or fully out, which is what you want before ps_modify_selection feathering. True lets Photoshop feather the focus falloff itself, useful when the subject edge is genuinely gradual (hair, fur, motion).',
|
|
332
|
+
default: false,
|
|
333
|
+
},
|
|
334
|
+
selection_type: selectionTypeFragment,
|
|
335
|
+
},
|
|
336
|
+
};
|
|
319
337
|
const modifyEdgeSchema = {
|
|
320
338
|
type: 'object',
|
|
321
339
|
properties: {
|
|
@@ -376,6 +394,12 @@ const selectionPreviewSchema = {
|
|
|
376
394
|
maximum: 4096,
|
|
377
395
|
default: 800,
|
|
378
396
|
},
|
|
397
|
+
image: {
|
|
398
|
+
type: 'string',
|
|
399
|
+
enum: ['overlay', 'mask', 'both'],
|
|
400
|
+
default: 'overlay',
|
|
401
|
+
description: "Which rendered image(s) to return inline. 'overlay' (default): a 50% red wash over the selected area (Quick Mask-style — most intuitive). 'mask': a B/W mask (black=selected, white=not) instead. 'both': the overlay followed by the mask, for when you need to compare them side by side. selection_info is returned regardless of this choice.",
|
|
402
|
+
},
|
|
379
403
|
},
|
|
380
404
|
};
|
|
381
405
|
const saveSelectionToChannelSchema = {
|
|
@@ -437,6 +461,7 @@ const SELECT_MODES = [
|
|
|
437
461
|
'skin_tones',
|
|
438
462
|
'out_of_gamut',
|
|
439
463
|
'polygon',
|
|
464
|
+
'focus_area',
|
|
440
465
|
];
|
|
441
466
|
const SELECT_INPUT_SCHEMA = {
|
|
442
467
|
type: 'object',
|
|
@@ -455,8 +480,9 @@ const SELECT_INPUT_SCHEMA = {
|
|
|
455
480
|
'skin_tones: select skin-coloured pixels (+fuzziness; use_faces=true adds face-aware refinement). ' +
|
|
456
481
|
'out_of_gamut: select colours outside the printable CMYK gamut (no params). ' +
|
|
457
482
|
'polygon: points [{x,y},...] in ABSOLUTE document pixels (min 3, auto-closes) — covers polygonal/freehand lasso. Coordinate-driven: you must know the pixel positions (use ps_inspect / ps_get_preview to aim, or ps_path create_from_placement → load_as_selection for a grounded outline). ' +
|
|
483
|
+
'focus_area: select what the lens rendered SHARP by depth of field, not by subject or colour (+in_focus_radius, soft_mask) — takes no coordinates; check whole_canvas_selected in the result before trusting it. ' +
|
|
458
484
|
'rectangle/ellipse/magic_wand also take a grounded `placement` instead of raw coords (region → the bbox; point → the wand click). ' +
|
|
459
|
-
'rectangle/ellipse/polygon/color_range/luminance_range/magic_wand/skin_tones/out_of_gamut also take selection_type to combine with an existing selection.',
|
|
485
|
+
'rectangle/ellipse/polygon/focus_area/color_range/luminance_range/magic_wand/skin_tones/out_of_gamut also take selection_type to combine with an existing selection.',
|
|
460
486
|
},
|
|
461
487
|
...selectRectangleSchema.properties,
|
|
462
488
|
...selectColorRangeSchema.properties,
|
|
@@ -464,6 +490,7 @@ const SELECT_INPUT_SCHEMA = {
|
|
|
464
490
|
...magicWandSchema.properties,
|
|
465
491
|
...colorPresetSchema.properties,
|
|
466
492
|
...selectPolygonSchema.properties,
|
|
493
|
+
...selectFocusAreaSchema.properties,
|
|
467
494
|
placement: {
|
|
468
495
|
...PLACEMENT_SCHEMA,
|
|
469
496
|
description: 'Grounded coordinates (rectangle/ellipse/magic_wand): NAME anchors + a relation instead of guessing pixels. rectangle/ellipse ← a REGION relation (inside/gap) → the selection bounding box; magic_wand ← a POINT relation (centroid/extremum/grid) → the click. Verified by the objective gate; wins over the raw edges/x-y. See the placement-resolver tool, when this build has one, for the vocabulary.',
|
|
@@ -580,7 +607,7 @@ export function createSelectionTools(connection, snippetClient, client = new Onn
|
|
|
580
607
|
{
|
|
581
608
|
tool: {
|
|
582
609
|
name: 'ps_select',
|
|
583
|
-
description: 'Create a NEW selection — choose with `mode`. (To edit the CURRENT selection instead — including growing it by colour similarity — use ps_modify_selection.) `all` selects the canvas; `none` deselects; `inverse` inverts the current selection (e.g. select the subject, then inverse to act on the background). `rectangle` (left/top/right/bottom, optional feather_px to avoid hard block-edges in smooth sky). `ellipse` (left/top/right/bottom bounding box + anti_alias — circles/ovals). `color_range` (target red/green/blue + fuzziness — "select all the red / skin tones"). `luminance_range` (highlights/shadows/midtones — foundation for glow / dodge-burn). `magic_wand` (click x/y + tolerance, contiguous). `grow` / `similar` are DEPRECATED here (they act on the CURRENT selection, not a new one) — use ps_modify_selection(op=grow|similar) instead; kept for one release for backward compatibility, identical behaviour. rectangle/ellipse/magic_wand also accept a grounded `placement` (NAME a region/point instead of guessing pixels — resolved + gate-verified). The geometric/color/wand modes take selection_type (replace|add|subtract|intersect) to combine with an existing selection and return a rich selection_info bundle — verify it (or ps_get_selection_preview) before committing to a mask.',
|
|
610
|
+
description: 'Create a NEW selection — choose with `mode`. (To edit the CURRENT selection instead — including growing it by colour similarity — use ps_modify_selection.) `all` selects the canvas; `none` deselects; `inverse` inverts the current selection (e.g. select the subject, then inverse to act on the background). `rectangle` (left/top/right/bottom, optional feather_px to avoid hard block-edges in smooth sky). `ellipse` (left/top/right/bottom bounding box + anti_alias — circles/ovals). `color_range` (target red/green/blue + fuzziness — "select all the red / skin tones"). `luminance_range` (highlights/shadows/midtones — foundation for glow / dodge-burn). `magic_wand` (click x/y + tolerance, contiguous). `focus_area` selects by depth of field rather than subject or colour (+in_focus_radius, soft_mask) — check whole_canvas_selected/warning in the result before trusting it. `grow` / `similar` are DEPRECATED here (they act on the CURRENT selection, not a new one) — use ps_modify_selection(op=grow|similar) instead; kept for one release for backward compatibility, identical behaviour. rectangle/ellipse/magic_wand also accept a grounded `placement` (NAME a region/point instead of guessing pixels — resolved + gate-verified). The geometric/color/wand modes take selection_type (replace|add|subtract|intersect) to combine with an existing selection and return a rich selection_info bundle — verify it (or ps_get_selection_preview) before committing to a mask.',
|
|
584
611
|
inputSchema: SELECT_INPUT_SCHEMA,
|
|
585
612
|
outputSchema: {
|
|
586
613
|
type: 'object',
|
|
@@ -606,6 +633,24 @@ export function createSelectionTools(connection, snippetClient, client = new Onn
|
|
|
606
633
|
preset: { type: 'string' },
|
|
607
634
|
point_count: { type: 'number' },
|
|
608
635
|
placement: { type: 'object' },
|
|
636
|
+
strategy_used: {
|
|
637
|
+
type: 'string',
|
|
638
|
+
description: 'mode=focus_area: "executeAction:focusMask".',
|
|
639
|
+
},
|
|
640
|
+
in_focus_radius: { type: 'number', description: 'mode=focus_area: radius used.' },
|
|
641
|
+
soft_mask: { type: 'boolean', description: 'mode=focus_area: soft_mask used.' },
|
|
642
|
+
active_layer_temporarily_changed: {
|
|
643
|
+
type: 'boolean',
|
|
644
|
+
description: 'mode=focus_area: true if the active layer was not an ordinary pixel layer and detection was temporarily retargeted to the bottom layer. Restored before return.',
|
|
645
|
+
},
|
|
646
|
+
whole_canvas_selected: {
|
|
647
|
+
type: 'boolean',
|
|
648
|
+
description: 'mode=focus_area: true when the RAW detection (before any selection_type combine) covered essentially the entire canvas — usually a non-result. selection_info reports the FINAL, post-combine selection and the two can legitimately disagree.',
|
|
649
|
+
},
|
|
650
|
+
warning: {
|
|
651
|
+
type: ['string', 'null'],
|
|
652
|
+
description: 'mode=focus_area: set when whole_canvas_selected is true.',
|
|
653
|
+
},
|
|
609
654
|
selection_info: selectionInfoFragment,
|
|
610
655
|
},
|
|
611
656
|
},
|
|
@@ -660,7 +705,7 @@ export function createSelectionTools(connection, snippetClient, client = new Onn
|
|
|
660
705
|
{
|
|
661
706
|
tool: {
|
|
662
707
|
name: 'ps_get_selection_preview',
|
|
663
|
-
description:
|
|
708
|
+
description: "Render an inline JPEG so the agent can visually verify what is currently selected: by default a red-wash OVERLAY (50% red over the selected area, Quick Mask-style — most intuitive); pass `image:'mask'` for a B/W MASK (black = selected, white = not) instead, or `image:'both'` for both. selection_info is always returned regardless of `image`. Heavier than the selection_info bundle alone (~2-4s) — call this when the stats look off or before committing a mask. Does NOT modify the source document.",
|
|
664
709
|
inputSchema: selectionPreviewSchema,
|
|
665
710
|
outputSchema: {
|
|
666
711
|
type: 'object',
|
|
@@ -673,7 +718,7 @@ export function createSelectionTools(connection, snippetClient, client = new Onn
|
|
|
673
718
|
},
|
|
674
719
|
},
|
|
675
720
|
annotations: {
|
|
676
|
-
title: 'Get Selection Preview
|
|
721
|
+
title: 'Get Selection Preview',
|
|
677
722
|
readOnlyHint: true,
|
|
678
723
|
idempotentHint: true,
|
|
679
724
|
},
|
|
@@ -817,42 +862,6 @@ export function createSelectionTools(connection, snippetClient, client = new Onn
|
|
|
817
862
|
},
|
|
818
863
|
handler: async (args) => selectSky(connection, snippetClient, args),
|
|
819
864
|
},
|
|
820
|
-
{
|
|
821
|
-
tool: {
|
|
822
|
-
name: 'ps_select_focus_area',
|
|
823
|
-
description: "Run Photoshop's \"Focus Area\" — select what the lens rendered SHARP, by depth of field rather than by subject or colour. Use it when the thing you want is defined by focus and not by what it is: lifting a subject off a bokeh background, masking the in-focus plane of a macro shot, or grabbing a shallow-depth foreground that Select Subject splits badly. Takes NO coordinates. in_focus_radius widens (higher) or narrows (lower) what counts as sharp; soft_mask=true gives feathered edges instead of a hard boundary. Analyses the ACTIVE layer, so target the photographic pixels you mean. If the active layer is not a raster layer at all (adjustment, smart object, text, shape), detection is retargeted to the bottom layer and active_layer_temporarily_changed comes back true — check it, because the analysed layer was then NOT the one you selected. An EMPTY raster layer cannot be told apart by kind, so it is not retargeted; it surfaces instead as an error or as whole_canvas_selected. whole_canvas_selected and warning diagnose Focus Area's RAW detection, measured BEFORE any selection_type combine — a uniformly sharp image (or too high an in_focus_radius) trips them even when combining then folds the result down to something small. Check whole_canvas_selected first; selection_info separately reports the FINAL, post-combine selection and can disagree with it by design (e.g. selection_type='subtract' against an existing selection), so read selection_info for what actually got selected, not as a substitute for whole_canvas_selected.",
|
|
824
|
-
inputSchema: selectFocusAreaSchema,
|
|
825
|
-
outputSchema: {
|
|
826
|
-
type: 'object',
|
|
827
|
-
properties: {
|
|
828
|
-
selected: { type: 'boolean' },
|
|
829
|
-
method: { type: 'string' },
|
|
830
|
-
strategy_used: { type: 'string' },
|
|
831
|
-
in_focus_radius: { type: 'number' },
|
|
832
|
-
soft_mask: { type: 'boolean' },
|
|
833
|
-
active_layer_temporarily_changed: {
|
|
834
|
-
type: 'boolean',
|
|
835
|
-
description: 'True if the active layer was not an ordinary pixel layer and detection was temporarily retargeted to the bottom layer. The original active layer is restored before return.',
|
|
836
|
-
},
|
|
837
|
-
whole_canvas_selected: {
|
|
838
|
-
type: 'boolean',
|
|
839
|
-
description: "True when Focus Area's RAW detection covered essentially the entire canvas — usually a non-result (radius too high, or nothing photographic to analyse). Measured BEFORE combining with any prior selection, so it describes the detection step, not the final selection: selection_info reports the FINAL, post-combine result and the two can legitimately disagree, e.g. selection_type='subtract' against an existing selection can leave this true while selection_info.area_percent is well under 100.",
|
|
840
|
-
},
|
|
841
|
-
warning: {
|
|
842
|
-
type: ['string', 'null'],
|
|
843
|
-
description: 'Set when whole_canvas_selected is true. Also describes the RAW detection, not the final post-combine selection.',
|
|
844
|
-
},
|
|
845
|
-
selection_type: { type: 'string' },
|
|
846
|
-
selection_info: selectionInfoFragment,
|
|
847
|
-
},
|
|
848
|
-
},
|
|
849
|
-
annotations: {
|
|
850
|
-
title: 'Select Focus Area',
|
|
851
|
-
idempotentHint: true,
|
|
852
|
-
},
|
|
853
|
-
},
|
|
854
|
-
handler: async (args) => selectFocusArea(connection, snippetClient, args),
|
|
855
|
-
},
|
|
856
865
|
];
|
|
857
866
|
}
|
|
858
867
|
const selectSubjectSchema = {
|
|
@@ -877,72 +886,77 @@ const selectSkySchema = {
|
|
|
877
886
|
selection_type: selectionTypeFragment,
|
|
878
887
|
},
|
|
879
888
|
};
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
in_focus_radius: {
|
|
884
|
-
type: 'number',
|
|
885
|
-
description: 'How much blur still counts as "in focus", in pixels. Higher pulls more of the soft transition zone into the selection; lower keeps only the crisply resolved plane. 4.07 is the Photoshop dialog default and a sane starting point. The useful band is narrow, and a radius well above the default selects the entire frame — so move in small steps and CHECK the returned area_percent and whole_canvas_selected: a selection covering essentially everything means the radius is too high and the result is worthless, even though the call reports success.',
|
|
886
|
-
default: 4.07,
|
|
887
|
-
minimum: 0.1,
|
|
888
|
-
maximum: 15,
|
|
889
|
-
},
|
|
890
|
-
soft_mask: {
|
|
891
|
-
type: 'boolean',
|
|
892
|
-
description: 'False (default) yields a hard-edged selection — every pixel fully in or fully out, which is what you want before ps_modify_selection feathering. True lets Photoshop feather the focus falloff itself, useful when the subject edge is genuinely gradual (hair, fur, motion).',
|
|
893
|
-
default: false,
|
|
894
|
-
},
|
|
895
|
-
selection_type: selectionTypeFragment,
|
|
896
|
-
},
|
|
897
|
-
};
|
|
889
|
+
function selectionInfoOf(result) {
|
|
890
|
+
return result.selection_info;
|
|
891
|
+
}
|
|
898
892
|
async function selectFocusArea(connection, snippetClient, rawArgs) {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
schema: selectFocusAreaSchema,
|
|
904
|
-
snippet: 'selectFocusArea',
|
|
905
|
-
errorPrefix: 'Error running Focus Area selection',
|
|
906
|
-
timeoutMs: SELECT_FOCUS_AREA_TIMEOUT_MS,
|
|
907
|
-
params: (args) => ({
|
|
893
|
+
try {
|
|
894
|
+
const args = validateArgs(selectFocusAreaSchema, rawArgs);
|
|
895
|
+
const selectionType = normalizeSelectionType(args.selection_type);
|
|
896
|
+
const script = await snippetClient.build('selectFocusArea', {
|
|
908
897
|
inFocusRadius: args.in_focus_radius ?? 4.07,
|
|
909
898
|
softMask: args.soft_mask ?? false,
|
|
910
|
-
selectionType
|
|
911
|
-
})
|
|
912
|
-
|
|
913
|
-
|
|
899
|
+
selectionType,
|
|
900
|
+
});
|
|
901
|
+
const result = (await runScript(connection, script, SELECT_FOCUS_AREA_TIMEOUT_MS));
|
|
902
|
+
let text = describeSelectionFacts(`Focus Area selection (${selectionType})`, selectionInfoOf(result));
|
|
903
|
+
if (result.warning) {
|
|
904
|
+
text += ` WARNING: ${String(result.warning)}`;
|
|
905
|
+
}
|
|
906
|
+
return {
|
|
907
|
+
content: [{ type: 'text', text }],
|
|
908
|
+
structuredContent: result,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
catch (error) {
|
|
912
|
+
return toolErrorResult('Error running Focus Area selection', error);
|
|
913
|
+
}
|
|
914
914
|
}
|
|
915
915
|
async function selectSubject(connection, snippetClient, rawArgs) {
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
schema: selectSubjectSchema,
|
|
921
|
-
snippet: 'selectSubject',
|
|
922
|
-
errorPrefix: 'Error running Select Subject',
|
|
923
|
-
timeoutMs: SELECT_SUBJECT_TIMEOUT_MS,
|
|
924
|
-
params: (args) => ({
|
|
916
|
+
try {
|
|
917
|
+
const args = validateArgs(selectSubjectSchema, rawArgs);
|
|
918
|
+
const selectionType = normalizeSelectionType(args.selection_type);
|
|
919
|
+
const script = await snippetClient.build('selectSubject', {
|
|
925
920
|
sampleAllLayers: args.sample_all_layers ?? true,
|
|
926
|
-
selectionType
|
|
927
|
-
})
|
|
928
|
-
|
|
929
|
-
|
|
921
|
+
selectionType,
|
|
922
|
+
});
|
|
923
|
+
const result = (await runScript(connection, script, SELECT_SUBJECT_TIMEOUT_MS));
|
|
924
|
+
return {
|
|
925
|
+
content: [
|
|
926
|
+
{
|
|
927
|
+
type: 'text',
|
|
928
|
+
text: describeSelectionFacts(`Select Subject (${selectionType})`, selectionInfoOf(result)),
|
|
929
|
+
},
|
|
930
|
+
],
|
|
931
|
+
structuredContent: result,
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
catch (error) {
|
|
935
|
+
return toolErrorResult('Error running Select Subject', error);
|
|
936
|
+
}
|
|
930
937
|
}
|
|
931
938
|
async function selectSky(connection, snippetClient, rawArgs) {
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
schema: selectSkySchema,
|
|
937
|
-
snippet: 'selectSky',
|
|
938
|
-
errorPrefix: 'Error running Select Sky',
|
|
939
|
-
timeoutMs: SELECT_SKY_TIMEOUT_MS,
|
|
940
|
-
params: (args) => ({
|
|
939
|
+
try {
|
|
940
|
+
const args = validateArgs(selectSkySchema, rawArgs);
|
|
941
|
+
const selectionType = normalizeSelectionType(args.selection_type);
|
|
942
|
+
const script = await snippetClient.build('selectSky', {
|
|
941
943
|
sampleAllLayers: args.sample_all_layers ?? true,
|
|
942
|
-
selectionType
|
|
943
|
-
})
|
|
944
|
-
|
|
945
|
-
|
|
944
|
+
selectionType,
|
|
945
|
+
});
|
|
946
|
+
const result = (await runScript(connection, script, SELECT_SKY_TIMEOUT_MS));
|
|
947
|
+
return {
|
|
948
|
+
content: [
|
|
949
|
+
{
|
|
950
|
+
type: 'text',
|
|
951
|
+
text: describeSelectionFacts(`Select Sky (${selectionType})`, selectionInfoOf(result)),
|
|
952
|
+
},
|
|
953
|
+
],
|
|
954
|
+
structuredContent: result,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
catch (error) {
|
|
958
|
+
return toolErrorResult('Error running Select Sky', error);
|
|
959
|
+
}
|
|
946
960
|
}
|
|
947
961
|
async function select(connection, snippetClient, detClient, rawArgs) {
|
|
948
962
|
const mode = rawArgs.mode;
|
|
@@ -974,6 +988,8 @@ async function select(connection, snippetClient, detClient, rawArgs) {
|
|
|
974
988
|
return selectColorPreset(connection, snippetClient, 'out_of_gamut', rest);
|
|
975
989
|
case 'polygon':
|
|
976
990
|
return selectPolygon(connection, snippetClient, rest);
|
|
991
|
+
case 'focus_area':
|
|
992
|
+
return selectFocusArea(connection, snippetClient, rest);
|
|
977
993
|
default:
|
|
978
994
|
return unknownDiscriminator('select mode', mode, SELECT_MODES);
|
|
979
995
|
}
|
|
@@ -1038,6 +1054,25 @@ export function normalizeSelectionType(raw) {
|
|
|
1038
1054
|
? v
|
|
1039
1055
|
: 'replace';
|
|
1040
1056
|
}
|
|
1057
|
+
async function readSelectionInfo(connection, snippetClient) {
|
|
1058
|
+
const script = await snippetClient.build('getSelectionState');
|
|
1059
|
+
return (await runScript(connection, script));
|
|
1060
|
+
}
|
|
1061
|
+
function describeSelectionFacts(label, info) {
|
|
1062
|
+
if (!info || typeof info.has_selection !== 'boolean') {
|
|
1063
|
+
return `${label} complete — selection facts unknown (the result carried no readable selection_info).`;
|
|
1064
|
+
}
|
|
1065
|
+
if (!info.has_selection) {
|
|
1066
|
+
return `${label} completed but the resulting selection is empty.`;
|
|
1067
|
+
}
|
|
1068
|
+
if (!info.bounds) {
|
|
1069
|
+
const reason = typeof info.error === 'string' ? ` (${info.error})` : '';
|
|
1070
|
+
return `${label} complete — a selection exists but its bounds/coverage could not be measured${reason}.`;
|
|
1071
|
+
}
|
|
1072
|
+
const b = info.bounds;
|
|
1073
|
+
const pct = (info.area_percent ?? 0).toFixed(1);
|
|
1074
|
+
return `${label} complete — selection (${b.left}, ${b.top}) to (${b.right}, ${b.bottom}), ${pct}% of canvas.`;
|
|
1075
|
+
}
|
|
1041
1076
|
function describeMaskOutcome(result) {
|
|
1042
1077
|
if (result.maskCreated)
|
|
1043
1078
|
return 'Layer mask created from selection';
|
|
@@ -1395,8 +1430,7 @@ async function magicWand(connection, snippetClient, detClient, rawArgs) {
|
|
|
1395
1430
|
}
|
|
1396
1431
|
export async function getSelectionInfoHandler(connection, snippetClient) {
|
|
1397
1432
|
try {
|
|
1398
|
-
const
|
|
1399
|
-
const result = (await runScript(connection, script));
|
|
1433
|
+
const result = (await readSelectionInfo(connection, snippetClient));
|
|
1400
1434
|
const summary = result.has_selection
|
|
1401
1435
|
? `Active selection: ${(result.area_percent ?? 0).toFixed(1)}% of canvas (${(result.pixel_count ?? 0).toLocaleString()} px), bounds-fill ratio ${(result.bounds_fill_ratio ?? 0).toFixed(2)}, edge complexity ${(result.edge_complexity ?? 0).toFixed(2)}`
|
|
1402
1436
|
: 'No active selection.';
|
|
@@ -1413,6 +1447,7 @@ async function getSelectionPreview(connection, snippetClient, rawArgs) {
|
|
|
1413
1447
|
try {
|
|
1414
1448
|
const args = validateArgs(selectionPreviewSchema, rawArgs);
|
|
1415
1449
|
const maxDimension = args.max_dimension ?? 800;
|
|
1450
|
+
const image = args.image ?? 'overlay';
|
|
1416
1451
|
const dir = await TempDir.create('editmamei-sel-preview-');
|
|
1417
1452
|
try {
|
|
1418
1453
|
const overlayPath = dir.path('overlay.jpg');
|
|
@@ -1436,17 +1471,28 @@ async function getSelectionPreview(connection, snippetClient, rawArgs) {
|
|
|
1436
1471
|
}
|
|
1437
1472
|
const overlayBytes = await readFile(overlayPath);
|
|
1438
1473
|
const maskBytes = await readFile(maskPath);
|
|
1439
|
-
const
|
|
1440
|
-
|
|
1474
|
+
const text = image === 'both'
|
|
1475
|
+
? `Selection preview rendered. Image 1 = overlay (red wash on selected area). Image 2 = mask (black=selected, white=not).`
|
|
1476
|
+
: image === 'mask'
|
|
1477
|
+
? `Selection preview rendered. Image 1 = mask (black=selected, white=not).`
|
|
1478
|
+
: `Selection preview rendered. Image 1 = overlay (red wash on selected area).`;
|
|
1479
|
+
const content = [{ type: 'text', text }];
|
|
1480
|
+
if (image === 'overlay' || image === 'both') {
|
|
1481
|
+
content.push({
|
|
1482
|
+
type: 'image',
|
|
1483
|
+
data: overlayBytes.toString('base64'),
|
|
1484
|
+
mimeType: 'image/jpeg',
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
if (image === 'mask' || image === 'both') {
|
|
1488
|
+
content.push({
|
|
1489
|
+
type: 'image',
|
|
1490
|
+
data: maskBytes.toString('base64'),
|
|
1491
|
+
mimeType: 'image/jpeg',
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1441
1494
|
return {
|
|
1442
|
-
content
|
|
1443
|
-
{
|
|
1444
|
-
type: 'text',
|
|
1445
|
-
text: `Selection preview rendered. Image 1 = overlay (red wash on selected area). Image 2 = mask (black=selected, white=not).`,
|
|
1446
|
-
},
|
|
1447
|
-
{ type: 'image', data: overlayB64, mimeType: 'image/jpeg' },
|
|
1448
|
-
{ type: 'image', data: maskB64, mimeType: 'image/jpeg' },
|
|
1449
|
-
],
|
|
1495
|
+
content,
|
|
1450
1496
|
structuredContent: {
|
|
1451
1497
|
rendered: true,
|
|
1452
1498
|
max_dimension: result.max_dimension ?? maxDimension,
|