ps_apply_brush_stroke
Apply a Photoshop brush stroke along a defined path using any of 16 brush-family tools, from retouching (heal, clone, dodge, burn) to painting, with control over size, hardness, opacity, and flow.
Instructions
Paint along a path with one of PS's 16 brush-family tools — supply the path EITHER as an anchor-relational placement (preferred: a path relation → the stroke traces a resolved, gate-verified curve along a traced edge / landmark / between two anchors, no pixel-guessing) OR as an explicit path list of anchor points — the retouch tools (healing_brush, clone_stamp, burn, dodge, blur, sharpen, smudge), the paint family (brush, pencil, eraser), and the specialty tools (pattern_stamp, art_history_brush, history_brush, color_replacement, background_eraser, sponge). The path parameter takes a list of anchor points with optional bezier handles, so the same tool handles straight-line strokes, freeform curves, and closed shapes — all by varying the path geometry. Reach for this when: (a) cloning out a distraction along a specific shape (clone_stamp with a source_point + a path tracing the unwanted edge); (b) healing a scratch or seam (healing_brush with source_point); (c) dodging / burning to redirect tonal balance along a contour; (d) painting a freehand line into the canvas (brush + foreground_color). Active layer must be a normal pixel layer (background auto-promotes); rasterize adjustment/shape/text/smart-object layers first. Auto-duplicates — the original is preserved and a "Brush Stroke ()" copy receives the paint. Brush dynamics: hardness, opacity, and flow are independently settable via hardness_pct / opacity_pct / flow_pct (the live tool options are mutated before stroking and restored to the user's prior state in finally). Sampled brushes (custom shape-stamp presets) silently ignore hardness/diameter mutations — vary their character via brush_preset instead.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| path | No | Ordered list of anchor points the stroke traces. **Minimum 2 anchors.** Each anchor is `{x, y}` for a sharp corner OR `{x, y, in: [hx, hy], out: [hx, hy]}` for a smooth bezier point. The `in` and `out` handles MUST be positioned **tangent** to the curve at the anchor — `in` placed in the direction of the PREVIOUS anchor in the array, `out` placed in the direction of the NEXT anchor. Handles placed RADIALLY (toward/away from the shape's center) produce loops + concave curves instead of smooth convex ones. Mix corner + smooth points freely. Coordinates are document pixels; (0, 0) is top-left. Partial handles (only `in` OR only `out`) degrade to a sharp corner. **Recipes for common natural-stroke shapes** (compute these client-side and emit the resulting `[{x, y}, ...]` array): (1) **Hand-drawn straight line A→B with sketchy feel**: sample 8-30 evenly-spaced corner anchors along the line, then perturb each interior anchor by ±2-5px on the perpendicular axis — OR pass clean anchors and use `jitter_px` to apply the perturbation server-side (preferred — cheaper, deterministic). (2) **Sine wave A→B, amplitude a, periods n**: for i in 0..N, x_i = A.x + (i/N)*(B.x - A.x), y_i = A.y + a * sin((i/N) * 2π * n). Use 20-40 anchors for a smooth wave. (3) **Parabolic arc A→B peaking height h above midline**: for i in 0..N, t = i/N, x_i = lerp(A.x, B.x, t), y_i = lerp(A.y, B.y, t) - 4*h*t*(1-t). (4) **Canonical clockwise circle of radius r around (cx, cy)** with k = r * 0.5523: TOP `{x: cx, y: cy-r, in: [cx-k, cy-r], out: [cx+k, cy-r]}`, RIGHT `{x: cx+r, y: cy, in: [cx+r, cy-k], out: [cx+r, cy+k]}`, BOTTOM `{x: cx, y: cy+r, in: [cx+k, cy+r], out: [cx-k, cy+r]}`, LEFT `{x: cx-r, y: cy, in: [cx-r, cy+k], out: [cx-r, cy-k]}` — close with `closed: true`. (5) **Many short overlapping strokes for ink-on-paper texture**: chain multiple `apply_brush_stroke` calls along the same trajectory with small position offsets and varying `brush_size`; reads more natural than one long stroke. | |
| tool | Yes | Which brush-family tool to dispatch. Headline retouch options: `healing_brush` and `clone_stamp` (both REQUIRE `source_point` — set the sample location, then stroke the path); `burn` darkens; `dodge` lightens; `blur` smooths; `sharpen` enhances local contrast; `smudge` pushes pixels in the stroke direction. Paint family: `brush` (the standard paintbrush — honors `foreground_color`), `pencil` (hard-edge), `eraser`. Specialty: `pattern_stamp`, `art_history_brush`, `history_brush`, `color_replacement`, `background_eraser`, `sponge`. | |
| closed | No | When true, the path is closed (last anchor connects back to first) so the stroke forms a loop. Default false. | |
| flow_pct | No | Optional brush flow override (percent, 0-100). Routed through `currentToolOptions.flow` (top-level integer key). Flow controls how much paint each stroke step deposits — for paint-family tools (`brush`, `pencil`, `pattern_stamp`), low flow means the stroke needs multiple overlapping passes to reach full opacity; high flow saturates immediately. Restored to the user's original value in `finally`. | |
| jitter_px | No | Server-side hand-drawn perturbation. When > 0, the handler shifts every INTERIOR anchor (not the first, not the last — those stay exactly where you placed them) by a deterministic pseudo-random offset in `[-jitter_px, +jitter_px]` on each axis BEFORE stroking. Bezier handles ride along with their anchor so curve tangents are preserved. Same inputs produce the same emitted shape every call. **Reach for this when** you want a clean intent (mathematically perfect line, sine wave, arc) to read as hand-drawn ink without writing the jitter math yourself. Typical values: 2-5 for subtle ink texture, 5-10 for visibly sketchy, 10-20 for cartoonish/scratchy. Default 0 (no jitter). | |
| placement | No | ANCHOR-RELATIONAL stroke path (preferred over supplying pixels): a PATH relation — `along` a traced edge or a Pro face-mesh landmark curve, `offset-curve`, or `segment` between two anchors — so the stroke traces the resolved, gate-verified curve (the FULL polyline, not just endpoints: paint along the jaw / horizon / under-eye). Strokes ONLY if the gate PASSES. When set, `path` is ignored; tool/brush_size/source_point/colors/dynamics/jitter still apply. See ps_resolve_placement for the anchors + relation vocabulary. | |
| brush_size | Yes | Brush tip diameter in pixels. Typical retouch values: 12-30 for fine work, 50-100 for filling, 150+ for broad strokes. Verified scriptable via `setd Brsh.Trgt masterDiameter` in the 2026-06-09 spike. | |
| opacity_pct | No | Optional brush opacity override (percent, 0-100). Routed through `currentToolOptions.opacity` (a top-level integer key on the tool options descriptor, not nested inside the brush sub-descriptor). Applies to the WHOLE stroke uniformly — varying-opacity strokes require chaining multiple `apply_brush_stroke` calls. Restored to the user's original value in `finally`. | |
| brush_preset | No | Optional brush-preset name to select before stroking (e.g. "Soft Round", "Hard Round", or any custom preset the user has loaded). The preset's saved hardness, opacity, flow, and shape dynamics load with the slct dispatch — set the preset for a coherent character (soft vs hard, calligraphic vs round), then optionally override individual values via `hardness_pct` / `opacity_pct` / `flow_pct`. If the named preset is not installed, the tool falls back to "Soft Round" then "Hard Round" (both ship on every install) and reports which one landed via `preset_applied` in the result. | |
| hardness_pct | No | Optional brush hardness override (percent, 0-100). When set, mutates the live tool's hardness via the `currentToolOptions.brush.hardness` get-mutate-set pattern AFTER any brush_preset has loaded — so the preset's hardness is what you start from and this overrides it. **Computed brushes only**: Soft Round / Hard Round / Calligraphic / etc. accept the mutation; **sampled brushes** (custom shape-stamp presets loaded from .abr files) silently ignore it because their shape isn't parametric. The user's original tool options are restored in `finally` so this doesn't leak to their next non-LLM brush action. | |
| source_point | No | Sample point for `clone_stamp` / `healing_brush` — the pixel location PS samples from while the stroke progresses. REQUIRED for those two tools (unless `source_placement` names it instead). Ignored for all others. `layer_name` defaults to the active layer at call time when omitted. | |
| foreground_color | No | RGB foreground color for paint-family tools (`brush`, `pencil`). The retouch family (`burn`/`dodge`/`blur`/`sharpen`/`smudge`/`clone_stamp`/`healing_brush`) ignores foreground color — setting it on those tools is harmless but pointless. The user's previous foreground color is restored after the stroke completes. | |
| source_placement | No | Grounded alternative to `source_point` for `clone_stamp` / `healing_brush`: NAME the sample location (resolves to a POINT via the grounding resolver + objective gate — e.g. an `extremum` for the cleanest/smoothest nearby skin, a `grid` intersection, or a landmark point) instead of guessing pixels. Resolves to a POINT relation (centroid / midpoint / offset / extremum / grid / landmark point); the resolved point supplies `source_point` and WINS over an explicit `source_point`. Strokes only if the source gate PASSES. | |
| apply_to_active_layer | No | If false (default), the stroke is applied to a duplicate of the active layer named "<OpName> (<Original Name>)" — the original is preserved and the LLM can undo simply by deleting the copy. If true, the stroke bakes directly into the active layer (the historical destructive behavior). |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| tool | No | ||
| closed | No | ||
| anchors | No | ||
| context | No | ||
| stroked | No | ||
| placement | No | Present when the stroke path came from anchor-relational placement: the resolved curve + gate verdict. | |
| tool_type | No | ||
| brush_size | No | ||
| flow_applied | No | ||
| path_removed | No | ||
| size_applied | No | ||
| preset_applied | No | Name of the brush preset that actually landed — equal to `brush_preset` if installed, "Soft Round" / "Hard Round" if a fallback fired, null when no preset was requested. | |
| opacity_applied | No | ||
| stroke_envelope | No | The doc-pixel bbox the stroke should occupy (path bbox + brush radius) — the objective target to verify stroke occupancy against. | |
| target_was_copy | No | ||
| clone_source_set | No | ||
| hardness_applied | No | ||
| source_placement | No | Present when the clone/heal sample point came from a source_placement: the resolved point + gate verdict. | |
| target_layer_name | No | ||
| background_promoted | No | ||
| original_layer_name | No |