feat: add non-destructive adjustment layers
This commit is contained in:
@@ -8,6 +8,7 @@ import { getLayerMask } from "@core/layer-mask-utils";
|
||||
import type { RasterLayer } from "@core/raster-layer";
|
||||
import type { Layer } from "@core/layer";
|
||||
import type { LayerGroup } from "@core/layer-group";
|
||||
import type { AdjustmentLayer, ColorAdjustment } from "@core/adjustment-layer";
|
||||
import type { Command } from "./command";
|
||||
import { commandIds } from "./ids";
|
||||
|
||||
@@ -73,6 +74,8 @@ export type DocumentAddGroupLayerPayload = {
|
||||
parentGroupId?: LayerId;
|
||||
group: LayerGroup;
|
||||
};
|
||||
export type DocumentAddAdjustmentLayerPayload = { artboardId: ArtboardId; parentGroupId?: LayerId; layer: AdjustmentLayer };
|
||||
export type DocumentSetAdjustmentPayload = { layerId: LayerId; adjustment: ColorAdjustment };
|
||||
|
||||
export type DocumentMoveLayerPayload = {
|
||||
layerId: LayerId;
|
||||
@@ -353,10 +356,31 @@ export const documentAddGroupLayerCommand: Command<DocumentAddGroupLayerPayload>
|
||||
},
|
||||
};
|
||||
|
||||
export const documentAddAdjustmentLayerCommand: Command<DocumentAddAdjustmentLayerPayload> = {
|
||||
id: commandIds.documentAddAdjustmentLayer,
|
||||
name: "Add adjustment layer",
|
||||
execute({ state }, payload) {
|
||||
if (payload.parentGroupId || !validAdjustment(payload.layer.adjustment)) return state;
|
||||
return { ...state, document: insertLayer(state.document, payload.artboardId, undefined, payload.layer, 0), editor: { ...state.editor, selection: { artboardId: payload.artboardId, layerIds: [payload.layer.id] } } };
|
||||
},
|
||||
};
|
||||
|
||||
export const documentSetAdjustmentCommand: Command<DocumentSetAdjustmentPayload> = {
|
||||
id: commandIds.documentSetAdjustment,
|
||||
name: "Edit adjustment layer",
|
||||
execute({ state }, payload) {
|
||||
const location = findLayerLocation(state.document, payload.layerId);
|
||||
if (!location || location.layer.type !== "adjustment" || location.layer.locked || !validAdjustment(payload.adjustment)) return state;
|
||||
return { ...state, document: mapLayerInDocument(state.document, payload.layerId, (layer) => layer.type === "adjustment" ? { ...layer, adjustment: payload.adjustment } : layer) };
|
||||
},
|
||||
};
|
||||
|
||||
export const documentMoveLayerCommand: Command<DocumentMoveLayerPayload> = {
|
||||
id: commandIds.documentMoveLayer,
|
||||
name: "Move layer",
|
||||
execute({ state }, payload) {
|
||||
const source = findLayerLocation(state.document, payload.layerId);
|
||||
if (source?.layer.type === "adjustment" && payload.toParentGroupId) return state;
|
||||
if (payload.toParentGroupId && !findGroup(state.document, payload.toParentGroupId)) return state;
|
||||
|
||||
const removed = removeLayerFromDocument(state.document, payload.layerId);
|
||||
@@ -390,6 +414,7 @@ export const documentGroupLayersCommand: Command<DocumentGroupLayersPayload> = {
|
||||
const location = findLayerLocation(state.document, layerId);
|
||||
return location ? [location] : [];
|
||||
});
|
||||
if (requestedLocations.some((location) => location.layer.type === "adjustment")) return state;
|
||||
if (requestedLocations.length !== requestedIds.length) return state;
|
||||
|
||||
const firstLocation = requestedLocations[0];
|
||||
@@ -463,14 +488,14 @@ export const documentSetLayerSourceRectCommand: Command<DocumentSetLayerSourceRe
|
||||
name: "Crop layer",
|
||||
execute({ state }, payload) {
|
||||
const location = findLayerLocation(state.document, payload.layerId);
|
||||
if (!location || location.layer.type === "group" || location.layer.locked || location.layer.transform.rotation !== 0) return state;
|
||||
if (!location || location.layer.type === "group" || location.layer.type === "adjustment" || location.layer.locked || location.layer.transform.rotation !== 0) return state;
|
||||
const leaf = location.layer;
|
||||
const asset = state.document.assets.find((candidate) => candidate.id === leaf.assetId);
|
||||
if (!asset) return state;
|
||||
const sourceRect = payload.sourceRect ? clampSourceRect(payload.sourceRect, asset.intrinsicSize.w, asset.intrinsicSize.h) : undefined;
|
||||
if (payload.sourceRect && !sourceRect) return state;
|
||||
return { ...state, document: mapLayerInDocument(state.document, payload.layerId, (layer) => {
|
||||
if (layer.type === "group") return layer;
|
||||
if (layer.type === "group" || layer.type === "adjustment") return layer;
|
||||
if (sourceRect) return { ...layer, sourceRect };
|
||||
const uncropped = { ...layer };
|
||||
delete uncropped.sourceRect;
|
||||
@@ -549,6 +574,7 @@ export const documentSetLayerClippingMaskCommand: Command<DocumentSetLayerClippi
|
||||
const targetLocation = findLayerLocation(state.document, payload.layerId);
|
||||
const maskLocation = findLayerLocation(state.document, payload.maskLayerId);
|
||||
if (!targetLocation || !maskLocation) return state;
|
||||
if (targetLocation.layer.type === "adjustment") return state;
|
||||
if (targetLocation.artboardId !== maskLocation.artboardId || targetLocation.parentGroupId !== maskLocation.parentGroupId) return state;
|
||||
|
||||
const removed = removeLayerFromDocument(state.document, payload.layerId);
|
||||
@@ -575,7 +601,7 @@ export const documentAddLayerMaskCommand: Command<DocumentAddLayerMaskPayload> =
|
||||
name: "Add layer mask",
|
||||
execute({ state }, payload) {
|
||||
const targetLocation = findLayerLocation(state.document, payload.layerId);
|
||||
if (!targetLocation || targetLocation.layer.type === "group") return state;
|
||||
if (!targetLocation || targetLocation.layer.type === "group" || targetLocation.layer.type === "adjustment") return state;
|
||||
|
||||
const existingMaskId = getLayerMask(targetLocation.layer)?.maskLayerId;
|
||||
if (existingMaskId) {
|
||||
@@ -638,7 +664,7 @@ export const documentApplyLayerMaskOperationCommand: Command<DocumentApplyLayerM
|
||||
if (!payload.source.trim()) return state;
|
||||
|
||||
const maskLocation = findLayerLocation(state.document, payload.maskLayerId);
|
||||
if (!maskLocation || maskLocation.layer.type === "group") return state;
|
||||
if (!maskLocation || maskLocation.layer.type === "group" || maskLocation.layer.type === "adjustment") return state;
|
||||
if (!isReferencedMaskLayer(state.document, payload.maskLayerId)) return state;
|
||||
const maskAssetId = maskLocation.layer.assetId;
|
||||
|
||||
@@ -730,6 +756,8 @@ export const documentCommands = [
|
||||
documentAddImageLayerCommand,
|
||||
documentAddRasterLayerCommand,
|
||||
documentAddGroupLayerCommand,
|
||||
documentAddAdjustmentLayerCommand,
|
||||
documentSetAdjustmentCommand,
|
||||
documentMoveLayerCommand,
|
||||
documentGroupLayersCommand,
|
||||
documentUngroupLayerCommand,
|
||||
@@ -750,6 +778,10 @@ function validRect(rect: Rect): Rect | undefined {
|
||||
return [rect.x, rect.y, rect.w, rect.h].every(Number.isFinite) && rect.w >= 1 && rect.h >= 1 ? { ...rect } : undefined;
|
||||
}
|
||||
|
||||
function validAdjustment(value: ColorAdjustment): boolean {
|
||||
return [value.brightness, value.contrast, value.saturation, value.colorBalance.red, value.colorBalance.green, value.colorBalance.blue].every((number) => Number.isFinite(number) && number >= -1 && number <= 1);
|
||||
}
|
||||
|
||||
function clampSourceRect(rect: Rect, width: number, height: number): Rect | undefined {
|
||||
if (![rect.x, rect.y, rect.w, rect.h].every(Number.isFinite)) return undefined;
|
||||
const x = Math.max(0, Math.min(width - 1, rect.x));
|
||||
@@ -760,7 +792,7 @@ function clampSourceRect(rect: Rect, width: number, height: number): Rect | unde
|
||||
}
|
||||
|
||||
function scaleLayerTree(layers: Layer[], before: Rect, after: Rect, scaleX: number, scaleY: number): Layer[] {
|
||||
return layers.map((layer) => layer.type === "group" ? {
|
||||
return layers.map((layer) => layer.type === "adjustment" ? layer : layer.type === "group" ? {
|
||||
...layer,
|
||||
children: scaleLayerTree(layer.children, before, after, scaleX, scaleY),
|
||||
} : {
|
||||
|
||||
Reference in New Issue
Block a user