feat: add first-class text layers

This commit is contained in:
syntaxbullet
2026-07-11 12:38:34 +02:00
parent 606426c885
commit 37aa719047
33 changed files with 315 additions and 41 deletions

View File

@@ -31,7 +31,7 @@ export function TransformControls({ bounds, target, documentIndex, layerInfo, di
const layer = layerInfo?.layer;
const locked = layer?.locked ?? false;
const mask = layer ? getLayerMask(layer) : undefined;
const rotatedMaskEditingUnsupported = Boolean(layer && layer.type !== "group" && layer.type !== "adjustment" && layer.transform.rotation !== 0);
const rotatedMaskEditingUnsupported = Boolean(layer && (layer.type === "image" || layer.type === "raster") && layer.transform.rotation !== 0);
useEffect(() => {
setDraft(draftFromBounds(bounds));
@@ -102,12 +102,12 @@ export function TransformControls({ bounds, target, documentIndex, layerInfo, di
<button type="button" className={actionButtonClass()} disabled={locked} title={locked ? "Unlock the layer to duplicate it" : "Duplicate layer"} onClick={() => duplicateLayer(documentIndex, layerInfo!, dispatch)}>
<Copy size={20} /> Duplicate
</button>
{layer.type !== "group" && layer.type !== "adjustment" ? (
{layer.type === "image" || layer.type === "raster" ? (
<button type="button" className={actionButtonClass()} disabled={locked || layer.transform.rotation !== 0} title={layer.transform.rotation !== 0 ? "Reset rotation before cropping" : "Crop visible source pixels"} onClick={() => setCropOpen((open) => !open)}>
<Crop size={20} /> Crop
</button>
) : null}
{layer.type !== "group" && layer.type !== "adjustment" ? (
{layer.type === "image" || layer.type === "raster" ? (
<button
type="button"
className={actionButtonClass()}
@@ -122,7 +122,7 @@ export function TransformControls({ bounds, target, documentIndex, layerInfo, di
</button>
) : null}
{locked ? <span className="text-xs text-amber-200/70">Unlock to edit</span> : null}
{cropOpen && layer.type !== "group" && layer.type !== "adjustment" ? <CropEditor draft={cropDraft} setDraft={setCropDraft} onCancel={() => { setCropDraft(cropDraftFor(layerInfo, documentIndex)); setCropOpen(false); }} onReset={() => { dispatch(commandIds.documentSetLayerSourceRect, { layerId: layer.id }); setCropOpen(false); }} onApply={() => { const sourceRect = parseRectDraft(cropDraft); if (sourceRect) { dispatch(commandIds.documentSetLayerSourceRect, { layerId: layer.id, sourceRect }); setCropOpen(false); } }} /> : null}
{cropOpen && (layer.type === "image" || layer.type === "raster") ? <CropEditor draft={cropDraft} setDraft={setCropDraft} onCancel={() => { setCropDraft(cropDraftFor(layerInfo, documentIndex)); setCropOpen(false); }} onReset={() => { dispatch(commandIds.documentSetLayerSourceRect, { layerId: layer.id }); setCropOpen(false); }} onApply={() => { const sourceRect = parseRectDraft(cropDraft); if (sourceRect) { dispatch(commandIds.documentSetLayerSourceRect, { layerId: layer.id, sourceRect }); setCropOpen(false); } }} /> : null}
</>
) : target.type === "artboard" ? (
<>
@@ -154,7 +154,7 @@ function ArtboardResizeEditor({ draft, setDraft, onApply, onCancel }: { draft: {
}
function cropDraftFor(layerInfo: IndexedLayerInfo | undefined, index: DocumentReadIndex): CropDraft {
if (!layerInfo || layerInfo.layer.type === "group" || layerInfo.layer.type === "adjustment") return { x: "0", y: "0", w: "1", h: "1" };
if (!layerInfo || (layerInfo.layer.type !== "image" && layerInfo.layer.type !== "raster")) return { x: "0", y: "0", w: "1", h: "1" };
const asset = index.assetById.get(layerInfo.layer.assetId);
const rect = layerInfo.layer.sourceRect ?? { x: 0, y: 0, w: asset?.intrinsicSize.w ?? 1, h: asset?.intrinsicSize.h ?? 1 };
return draftFromBounds(rect);