diff --git a/core/asset.ts b/core/asset.ts index b9b514b..45f92c4 100644 --- a/core/asset.ts +++ b/core/asset.ts @@ -1,3 +1,4 @@ +import type { Size } from "./geometry"; import type { AssetId } from "./id"; export type Asset = { @@ -5,4 +6,5 @@ export type Asset = { name: string; mimeType: string; source: string; + intrinsicSize: Size; }; diff --git a/editor/transform-targets.test.ts b/editor/transform-targets.test.ts index 20d6438..2160422 100644 --- a/editor/transform-targets.test.ts +++ b/editor/transform-targets.test.ts @@ -6,14 +6,25 @@ const document: ImageDocument = { id: "d1", name: "Test", version: 1, - assets: [], + assets: [{ id: "asset-1", name: "Image", mimeType: "image/png", source: "asset://image", intrinsicSize: { w: 200, h: 100 } }], artboards: [ { id: "a1", name: "Artboard", bounds: { x: 0, y: 0, w: 100, h: 80 }, backgroundColor: "transparent", - layers: [], + layers: [ + { + id: "l1", + type: "image", + name: "Image Layer", + visible: true, + locked: false, + opacity: 1, + assetId: "asset-1", + transform: { position: { x: 10, y: 20 }, scale: { x: 0.5, y: 2 }, rotation: 0 }, + }, + ], }, ], }; @@ -27,6 +38,17 @@ describe("transform targets", () => { expect(resolveTransformTargetBounds(document, { type: "artboard", id: "a1" })).toEqual({ x: 0, y: 0, w: 100, h: 80 }); }); + test("resolves image layer bounds from intrinsic asset dimensions", () => { + expect(resolveTransformTargetBounds(document, { type: "layer", id: "l1" })).toEqual({ x: 10, y: 20, w: 100, h: 200 }); + }); + + test("applies image layer bounds to transform", () => { + const next = applyTransformTargetBounds(document, { type: "layer", id: "l1" }, { x: 30, y: 40, w: 400, h: 50 }); + const layer = next.artboards[0]?.layers[0]; + + expect(layer?.transform).toEqual({ position: { x: 30, y: 40 }, scale: { x: 2, y: 0.5 }, rotation: 0 }); + }); + test("applies artboard bounds", () => { const next = applyTransformTargetBounds(document, { type: "artboard", id: "a1" }, { x: 10, y: 20, w: 200, h: 160 }); expect(next.artboards[0]?.bounds).toEqual({ x: 10, y: 20, w: 200, h: 160 }); diff --git a/editor/transform-targets.ts b/editor/transform-targets.ts index 7210073..c9145c7 100644 --- a/editor/transform-targets.ts +++ b/editor/transform-targets.ts @@ -10,7 +10,7 @@ export function resolveTransformTargetBounds(document: ImageDocument, target: Tr return document.artboards.find((artboard) => artboard.id === target.id)?.bounds; case "layer": { const layer = findLayer(document, target.id); - return layer ? resolveLayerBounds(layer) : undefined; + return layer ? resolveLayerBounds(document, layer) : undefined; } } } @@ -23,7 +23,7 @@ export function applyTransformTargetBounds(document: ImageDocument, target: Tran artboards: document.artboards.map((artboard) => (artboard.id === target.id ? { ...artboard, bounds: { ...bounds } } : artboard)), }; case "layer": - return document; + return applyLayerBounds(document, target.id, bounds); } } @@ -33,6 +33,40 @@ export function selectedTransformTarget(document: ImageDocument, selection: { ar return undefined; } +function applyLayerBounds(document: ImageDocument, layerId: LayerId, bounds: Rect): ImageDocument { + return { + ...document, + artboards: document.artboards.map((artboard) => ({ + ...artboard, + layers: applyLayerBoundsInTree(document, artboard.layers, layerId, bounds), + })), + }; +} + +function applyLayerBoundsInTree(document: ImageDocument, layers: Layer[], layerId: LayerId, bounds: Rect): Layer[] { + return layers.map((layer) => { + if (layer.id === layerId && layer.type === "image") { + 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); @@ -54,15 +88,24 @@ function findLayerInTree(layers: Layer[], layerId: LayerId): Layer | undefined { return undefined; } -function resolveLayerBounds(layer: Layer): Rect | undefined { +function resolveLayerBounds(document: ImageDocument, layer: Layer): Rect | undefined { switch (layer.type) { case "group": return unionRects(layer.children.flatMap((child) => { - const bounds = resolveLayerBounds(child); + const bounds = resolveLayerBounds(document, child); return bounds ? [bounds] : []; })); - case "image": - return undefined; + case "image": { + 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, + }; + } } }