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:
163
commands/tool.ts
163
commands/tool.ts
@@ -1,4 +1,8 @@
|
||||
import type { LayerId } from "@core/id";
|
||||
import type { ImageDocument } from "@core/document";
|
||||
import type { Vec2D } from "@core/geometry";
|
||||
import type { LayerId, ArtboardId, AssetId } from "@core/id";
|
||||
import type { Layer } from "@core/layer";
|
||||
import type { MaskViewMode } from "@editor/state";
|
||||
import type { BrushSettings, ToolId } from "@editor/tools";
|
||||
import type { Command } from "./command";
|
||||
import { commandIds } from "./ids";
|
||||
@@ -9,8 +13,23 @@ export type ToolSetActivePayload = {
|
||||
|
||||
export type ToolSetBrushSettingsPayload = Partial<BrushSettings>;
|
||||
|
||||
export type ToolSetMaskEditLayerPayload = {
|
||||
layerId?: LayerId;
|
||||
export type ToolSetBrushPreviewPayload = { position: Vec2D } | undefined;
|
||||
|
||||
export type ToolSetBrushStrokePreviewPayload =
|
||||
| {
|
||||
layerId: LayerId;
|
||||
assetId: AssetId;
|
||||
source: string;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
export type ToolSetMaskViewModePayload = {
|
||||
mode: MaskViewMode;
|
||||
};
|
||||
|
||||
export type ToolEnterMaskEditPayload = {
|
||||
targetLayerId: LayerId;
|
||||
maskLayerId: LayerId;
|
||||
};
|
||||
|
||||
export const toolSetActiveCommand: Command<ToolSetActivePayload> = {
|
||||
@@ -26,6 +45,8 @@ export const toolSetActiveCommand: Command<ToolSetActivePayload> = {
|
||||
activeTool: payload.tool,
|
||||
interactionMode: { type: "tool", tool: payload.tool },
|
||||
},
|
||||
brushPreview: undefined,
|
||||
brushStrokePreview: undefined,
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -52,15 +73,107 @@ export const toolSetBrushSettingsCommand: Command<ToolSetBrushSettingsPayload> =
|
||||
},
|
||||
};
|
||||
|
||||
export const toolSetMaskEditLayerCommand: Command<ToolSetMaskEditLayerPayload> = {
|
||||
id: commandIds.toolSetMaskEditLayer,
|
||||
name: "Set mask edit layer",
|
||||
export const toolSetBrushPreviewCommand: Command<ToolSetBrushPreviewPayload> = {
|
||||
id: commandIds.toolSetBrushPreview,
|
||||
name: "Set brush preview",
|
||||
execute({ state }, payload) {
|
||||
if (!payload) {
|
||||
if (!state.editor.brushPreview) return state;
|
||||
return { ...state, editor: { ...state.editor, brushPreview: undefined } };
|
||||
}
|
||||
|
||||
const currentPosition = state.editor.brushPreview?.position;
|
||||
if (currentPosition && currentPosition.x === payload.position.x && currentPosition.y === payload.position.y) return state;
|
||||
|
||||
return {
|
||||
...state,
|
||||
editor: {
|
||||
...state.editor,
|
||||
maskEditLayerId: payload.layerId,
|
||||
brushPreview: { position: { ...payload.position } },
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const toolSetBrushStrokePreviewCommand: Command<ToolSetBrushStrokePreviewPayload> = {
|
||||
id: commandIds.toolSetBrushStrokePreview,
|
||||
name: "Set brush stroke preview",
|
||||
execute({ state }, payload) {
|
||||
if (!payload) {
|
||||
if (!state.editor.brushStrokePreview) return state;
|
||||
return { ...state, editor: { ...state.editor, brushStrokePreview: undefined } };
|
||||
}
|
||||
|
||||
const currentPreview = state.editor.brushStrokePreview;
|
||||
if (currentPreview?.layerId === payload.layerId && currentPreview.assetId === payload.assetId && currentPreview.source === payload.source) return state;
|
||||
|
||||
return {
|
||||
...state,
|
||||
editor: {
|
||||
...state.editor,
|
||||
brushStrokePreview: { layerId: payload.layerId, assetId: payload.assetId, source: payload.source },
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const toolSetMaskViewModeCommand: Command<ToolSetMaskViewModePayload> = {
|
||||
id: commandIds.toolSetMaskViewMode,
|
||||
name: "Set mask view mode",
|
||||
execute({ state }, payload) {
|
||||
if (!state.editor.maskEdit || state.editor.maskEdit.viewMode === payload.mode) return state;
|
||||
|
||||
return {
|
||||
...state,
|
||||
editor: {
|
||||
...state.editor,
|
||||
maskEdit: { ...state.editor.maskEdit, viewMode: payload.mode },
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const toolEnterMaskEditCommand: Command<ToolEnterMaskEditPayload> = {
|
||||
id: commandIds.toolEnterMaskEdit,
|
||||
name: "Enter mask edit",
|
||||
execute({ state }, payload) {
|
||||
const targetLocation = findLayerLocation(state.document, payload.targetLayerId);
|
||||
const maskLocation = findLayerLocation(state.document, payload.maskLayerId);
|
||||
if (!targetLocation || !maskLocation) return state;
|
||||
if (targetLocation.layer.clippingMask?.maskLayerId !== payload.maskLayerId) return state;
|
||||
if (maskLocation.layer.type === "group") return state;
|
||||
|
||||
return {
|
||||
...state,
|
||||
editor: {
|
||||
...state.editor,
|
||||
selection: { artboardId: targetLocation.artboardId, layerIds: [payload.targetLayerId] },
|
||||
maskEdit: { targetLayerId: payload.targetLayerId, maskLayerId: payload.maskLayerId },
|
||||
brushPreview: undefined,
|
||||
brushStrokePreview: undefined,
|
||||
tools: {
|
||||
...state.editor.tools,
|
||||
activeTool: "brush",
|
||||
interactionMode: { type: "tool", tool: "brush" },
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const toolExitMaskEditCommand: Command = {
|
||||
id: commandIds.toolExitMaskEdit,
|
||||
name: "Exit mask edit",
|
||||
execute({ state }) {
|
||||
if (!state.editor.maskEdit) return state;
|
||||
|
||||
return {
|
||||
...state,
|
||||
editor: {
|
||||
...state.editor,
|
||||
maskEdit: undefined,
|
||||
brushPreview: undefined,
|
||||
brushStrokePreview: undefined,
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -106,9 +219,43 @@ export const toolExitTemporaryPanCommand: Command = {
|
||||
},
|
||||
};
|
||||
|
||||
export const toolCommands = [toolSetActiveCommand, toolSetBrushSettingsCommand, toolSetMaskEditLayerCommand, toolEnterTemporaryPanCommand, toolExitTemporaryPanCommand] satisfies Command<unknown>[];
|
||||
export const toolCommands = [
|
||||
toolSetActiveCommand,
|
||||
toolSetBrushSettingsCommand,
|
||||
toolSetBrushPreviewCommand,
|
||||
toolSetBrushStrokePreviewCommand,
|
||||
toolSetMaskViewModeCommand,
|
||||
toolEnterMaskEditCommand,
|
||||
toolExitMaskEditCommand,
|
||||
toolEnterTemporaryPanCommand,
|
||||
toolExitTemporaryPanCommand,
|
||||
] satisfies Command<unknown>[];
|
||||
|
||||
function clampNumber(value: number, min: number, max: number) {
|
||||
if (!Number.isFinite(value)) return min;
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
type LayerLocation = {
|
||||
artboardId: ArtboardId;
|
||||
layer: Layer;
|
||||
};
|
||||
|
||||
function findLayerLocation(document: ImageDocument, layerId: LayerId): LayerLocation | undefined {
|
||||
for (const artboard of document.artboards) {
|
||||
const layer = findLayerInTree(artboard.layers, layerId);
|
||||
if (layer) return { artboardId: artboard.id, layer };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findLayerInTree(layers: readonly Layer[], layerId: LayerId): Layer | undefined {
|
||||
for (const layer of layers) {
|
||||
if (layer.id === layerId) return layer;
|
||||
if (layer.type === "group") {
|
||||
const child = findLayerInTree(layer.children, layerId);
|
||||
if (child) return child;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user