feat: enhance layer masking functionality and brush controls

- Refactor LayersSheet component to support mask editing state and improve layer visibility handling.
- Introduce functions to collect mask layer IDs and count display layers excluding masks.
- Update BrushControls to include mask view mode options and a Done button for exiting mask editing.
- Modify brush session handling to support brush previews when editing masks.
- Implement a new BrushPreviewRenderer for rendering brush strokes with visual feedback.
- Add document geometry utilities for transforming points and resolving layer bounds.
- Ensure proper cleanup of brush preview on pointer leave and other interactions.
This commit is contained in:
syntaxbullet
2026-07-03 20:49:29 +02:00
parent daaa2a6667
commit 4ad0bb8b2c
33 changed files with 1889 additions and 351 deletions

View File

@@ -1,5 +1,6 @@
import { commandIds } from "@commands/ids";
import type { AppStore } from "@editor/store";
import type { MaskViewMode } from "@editor/state";
import type { BrushSettings, ToolId } from "@editor/tools";
import { BottomControlDivider } from "./Divider";
import { bottomControlLabelClass } from "./styles";
@@ -7,15 +8,17 @@ import { bottomControlLabelClass } from "./styles";
export type BrushControlsProps = {
tool: Extract<ToolId, "brush" | "eraser">;
settings: BrushSettings;
editingMask?: boolean;
maskViewMode?: MaskViewMode;
dispatch: AppStore["dispatch"];
};
export function BrushControls({ tool, settings, dispatch }: BrushControlsProps) {
export function BrushControls({ tool, settings, editingMask = false, maskViewMode = "composite", dispatch }: BrushControlsProps) {
return (
<div className="flex w-full items-center justify-center gap-2 tabular-nums">
<span className="px-2 font-medium text-white/85">{tool === "eraser" ? "Eraser" : "Brush"}</span>
<span className="px-2 font-medium text-white/85">{editingMask ? `Mask · ${tool === "eraser" ? "Hide" : "Reveal"}` : tool === "eraser" ? "Eraser" : "Brush"}</span>
<BottomControlDivider />
{tool === "brush" ? (
{tool === "brush" && !editingMask ? (
<label className="flex items-center gap-1.5">
<span className={bottomControlLabelClass()}>Color</span>
<input
@@ -54,6 +57,33 @@ export function BrushControls({ tool, settings, dispatch }: BrushControlsProps)
/>
<span className="w-8 text-right text-white">{Math.round(settings.hardness)}</span>
</label>
{editingMask ? (
<>
<BottomControlDivider />
<label className="flex items-center gap-1.5">
<span className={bottomControlLabelClass()}>View</span>
<select
className="h-7 rounded-full border border-white/10 bg-black/70 px-2 text-white outline-none"
value={maskViewMode}
aria-label="Mask view mode"
onChange={(event) => dispatch(commandIds.toolSetMaskViewMode, { mode: event.target.value as MaskViewMode })}
>
<option value="composite">Image</option>
<option value="blackWhite">B/W</option>
<option value="alpha">Alpha</option>
<option value="overlay">Overlay</option>
</select>
</label>
<BottomControlDivider />
<button
type="button"
className="rounded-full bg-white px-3 py-1 font-medium text-black transition hover:bg-white/90"
onClick={() => dispatch(commandIds.toolExitMaskEdit, undefined)}
>
Done
</button>
</>
) : null}
</div>
);
}