import type { ImageDocument } from "@core/document"; import type { Rect } from "@core/geometry"; import type { Layer } from "@core/layer"; import type { ArtboardId, LayerId } from "@core/id"; import type { TransformTarget } from "./transform"; export function resolveTransformTargetBounds(document: ImageDocument, target: TransformTarget): Rect | undefined { switch (target.type) { case "artboard": return document.artboards.find((artboard) => artboard.id === target.id)?.bounds; case "layer": { const layer = findLayer(document, target.id); return layer ? resolveLayerBounds(document, layer) : undefined; } } } export function applyTransformTargetBounds(document: ImageDocument, target: TransformTarget, bounds: Rect): ImageDocument { switch (target.type) { case "artboard": return { ...document, artboards: document.artboards.map((artboard) => (artboard.id === target.id ? { ...artboard, bounds: { ...bounds } } : artboard)), }; case "layer": return applyLayerBounds(document, target.id, bounds); } } export function selectedTransformTarget(document: ImageDocument, selection: { artboardId?: ArtboardId; layerIds: LayerId[] }): TransformTarget | undefined { if (selection.layerIds.length === 1 && selection.layerIds[0]) return { type: "layer", id: selection.layerIds[0] }; if (selection.artboardId) return { type: "artboard", id: selection.artboardId }; return undefined; } function applyLayerBounds(document: ImageDocument, layerId: LayerId, bounds: Rect): ImageDocument { const layer = findLayer(document, layerId); const targetLayerIds = layer?.clippingMask ? [layerId, layer.clippingMask.maskLayerId] : [layerId]; return targetLayerIds.reduce( (nextDocument, targetLayerId) => ({ ...nextDocument, artboards: nextDocument.artboards.map((artboard) => ({ ...artboard, layers: applyLayerBoundsInTree(nextDocument, artboard.layers, targetLayerId, bounds), })), }), document, ); } function applyLayerBoundsInTree(document: ImageDocument, layers: Layer[], layerId: LayerId, bounds: Rect): Layer[] { return layers.map((layer) => { if (layer.id === layerId && (layer.type === "image" || layer.type === "raster")) { const asset = document.assets.find((candidate) => candidate.id === layer.assetId); if (!asset) return layer; return { ...layer, transform: { ...layer.transform, position: { x: bounds.x, y: bounds.y }, scale: { x: bounds.w / asset.intrinsicSize.w, y: bounds.h / asset.intrinsicSize.h, }, }, }; } if (layer.type === "group") return { ...layer, children: applyLayerBoundsInTree(document, layer.children, layerId, bounds) }; return layer; }); } function findLayer(document: ImageDocument, layerId: LayerId): Layer | undefined { for (const artboard of document.artboards) { const layer = findLayerInTree(artboard.layers, layerId); if (layer) return layer; } return undefined; } function findLayerInTree(layers: 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; } function resolveLayerBounds(document: ImageDocument, layer: Layer): Rect | undefined { switch (layer.type) { case "group": return unionRects(layer.children.flatMap((child) => { const bounds = resolveLayerBounds(document, child); return bounds ? [bounds] : []; })); case "image": case "raster": { const asset = document.assets.find((candidate) => candidate.id === layer.assetId); if (!asset) return undefined; return { x: layer.transform.position.x, y: layer.transform.position.y, w: asset.intrinsicSize.w * layer.transform.scale.x, h: asset.intrinsicSize.h * layer.transform.scale.y, }; } } } function unionRects(rects: Rect[]): Rect | undefined { if (rects.length === 0) return undefined; const minX = Math.min(...rects.map((rect) => rect.x)); const minY = Math.min(...rects.map((rect) => rect.y)); const maxX = Math.max(...rects.map((rect) => rect.x + rect.w)); const maxY = Math.max(...rects.map((rect) => rect.y + rect.h)); return { x: minX, y: minY, w: maxX - minX, h: maxY - minY }; }