feat: add crop and canvas resize workflows
This commit is contained in:
@@ -22,6 +22,12 @@ export type DocumentSetArtboardBoundsPayload = {
|
||||
bounds: Rect;
|
||||
};
|
||||
|
||||
export type DocumentResizeArtboardPayload = {
|
||||
id: ArtboardId;
|
||||
bounds: Rect;
|
||||
scaleContents: boolean;
|
||||
};
|
||||
|
||||
export type DocumentRemoveArtboardPayload = {
|
||||
id: ArtboardId;
|
||||
};
|
||||
@@ -104,6 +110,11 @@ export type DocumentSetLayerOpacityPayload = {
|
||||
opacity: number;
|
||||
};
|
||||
|
||||
export type DocumentSetLayerSourceRectPayload = {
|
||||
layerId: LayerId;
|
||||
sourceRect?: Rect;
|
||||
};
|
||||
|
||||
export type DocumentDuplicateLayerPayload = {
|
||||
layerId: LayerId;
|
||||
idByLayerId: Record<LayerId, LayerId>;
|
||||
@@ -189,6 +200,29 @@ export const documentSetArtboardBoundsCommand: Command<DocumentSetArtboardBounds
|
||||
},
|
||||
};
|
||||
|
||||
export const documentResizeArtboardCommand: Command<DocumentResizeArtboardPayload> = {
|
||||
id: commandIds.documentResizeArtboard,
|
||||
name: "Resize artboard",
|
||||
execute({ state }, payload) {
|
||||
const artboard = state.document.artboards.find((candidate) => candidate.id === payload.id);
|
||||
const bounds = validRect(payload.bounds);
|
||||
if (!artboard || artboard.locked || !bounds) return state;
|
||||
const scaleX = bounds.w / artboard.bounds.w;
|
||||
const scaleY = bounds.h / artboard.bounds.h;
|
||||
return {
|
||||
...state,
|
||||
document: {
|
||||
...state.document,
|
||||
artboards: state.document.artboards.map((candidate) => candidate.id !== payload.id ? candidate : {
|
||||
...candidate,
|
||||
bounds,
|
||||
layers: payload.scaleContents ? scaleLayerTree(candidate.layers, artboard.bounds, bounds, scaleX, scaleY) : candidate.layers,
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const documentRemoveArtboardCommand: Command<DocumentRemoveArtboardPayload> = {
|
||||
id: commandIds.documentRemoveArtboard,
|
||||
name: "Remove artboard",
|
||||
@@ -424,6 +458,27 @@ export const documentSetLayerOpacityCommand: Command<DocumentSetLayerOpacityPayl
|
||||
},
|
||||
};
|
||||
|
||||
export const documentSetLayerSourceRectCommand: Command<DocumentSetLayerSourceRectPayload> = {
|
||||
id: commandIds.documentSetLayerSourceRect,
|
||||
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;
|
||||
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 (sourceRect) return { ...layer, sourceRect };
|
||||
const uncropped = { ...layer };
|
||||
delete uncropped.sourceRect;
|
||||
return uncropped;
|
||||
}) };
|
||||
},
|
||||
};
|
||||
|
||||
export const documentDuplicateLayerCommand: Command<DocumentDuplicateLayerPayload> = {
|
||||
id: commandIds.documentDuplicateLayer,
|
||||
name: "Duplicate layer",
|
||||
@@ -665,6 +720,7 @@ export const documentRemoveLayerCommand: Command<DocumentRemoveLayerPayload> = {
|
||||
export const documentCommands = [
|
||||
documentAddArtboardCommand,
|
||||
documentSetArtboardBoundsCommand,
|
||||
documentResizeArtboardCommand,
|
||||
documentRemoveArtboardCommand,
|
||||
documentSetArtboardVisibleCommand,
|
||||
documentSetArtboardLockedCommand,
|
||||
@@ -681,6 +737,7 @@ export const documentCommands = [
|
||||
documentSetLayerVisibleCommand,
|
||||
documentSetLayerLockedCommand,
|
||||
documentSetLayerOpacityCommand,
|
||||
documentSetLayerSourceRectCommand,
|
||||
documentDuplicateLayerCommand,
|
||||
documentRenameLayerCommand,
|
||||
documentSetLayerClippingMaskCommand,
|
||||
@@ -688,3 +745,33 @@ export const documentCommands = [
|
||||
documentApplyLayerMaskOperationCommand,
|
||||
documentRemoveLayerMaskCommand,
|
||||
] satisfies Command<unknown>[];
|
||||
|
||||
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 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));
|
||||
const y = Math.max(0, Math.min(height - 1, rect.y));
|
||||
const w = Math.min(width - x, rect.w);
|
||||
const h = Math.min(height - y, rect.h);
|
||||
return w >= 1 && h >= 1 ? { x, y, w, h } : undefined;
|
||||
}
|
||||
|
||||
function scaleLayerTree(layers: Layer[], before: Rect, after: Rect, scaleX: number, scaleY: number): Layer[] {
|
||||
return layers.map((layer) => layer.type === "group" ? {
|
||||
...layer,
|
||||
children: scaleLayerTree(layer.children, before, after, scaleX, scaleY),
|
||||
} : {
|
||||
...layer,
|
||||
transform: {
|
||||
...layer.transform,
|
||||
position: {
|
||||
x: after.x + (layer.transform.position.x - before.x) * scaleX,
|
||||
y: after.y + (layer.transform.position.y - before.y) * scaleY,
|
||||
},
|
||||
scale: { x: layer.transform.scale.x * scaleX, y: layer.transform.scale.y * scaleY },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user