import { useEffect, useState, type Dispatch, type SetStateAction } from "react"; import { BoundingBox, Copy, MaskHappy } from "@phosphor-icons/react"; import { commandIds } from "@commands/ids"; import type { Rect } from "@core/geometry"; import type { AppStore } from "@editor/store"; import type { TransformTarget } from "@editor/transform"; import type { DocumentReadIndex, IndexedLayerInfo } from "@editor/document-indexes"; import { addLayerMask, duplicateLayer } from "@operations/document/layerActions"; import { getLayerMask } from "@core/layer-mask-utils"; import { BottomControlDivider } from "./Divider"; import { bottomControlFieldClass, bottomControlIconSlotClass, bottomControlInputClass, bottomControlLabelClass, bottomControlMenuClass } from "./styles"; export type TransformControlsProps = { bounds: Rect; target: TransformTarget; documentIndex: DocumentReadIndex; layerInfo?: IndexedLayerInfo; dispatch: AppStore["dispatch"]; }; type BoundsField = keyof Rect; export function TransformControls({ bounds, target, documentIndex, layerInfo, dispatch }: TransformControlsProps) { const [draft, setDraft] = useState(() => draftFromBounds(bounds)); const [opacityDraft, setOpacityDraft] = useState(() => String(Math.round((layerInfo?.layer.opacity ?? 1) * 100))); const [rotationDraft, setRotationDraft] = useState(() => rotationDegrees(layerInfo)); const layer = layerInfo?.layer; const locked = layer?.locked ?? false; const mask = layer ? getLayerMask(layer) : undefined; const rotatedMaskEditingUnsupported = Boolean(layer && layer.type !== "group" && layer.transform.rotation !== 0); useEffect(() => { setDraft(draftFromBounds(bounds)); }, [bounds.x, bounds.y, bounds.w, bounds.h]); useEffect(() => setOpacityDraft(String(Math.round((layer?.opacity ?? 1) * 100))), [layer?.id, layer?.opacity]); useEffect(() => setRotationDraft(rotationDegrees(layerInfo)), [layer?.id, layer?.transform.rotation]); const commitField = (field: BoundsField) => { const value = Number.parseFloat(draft[field]); if (!Number.isFinite(value)) { setDraft(draftFromBounds(bounds)); return; } if (locked) return; dispatch(commandIds.transformSetBounds, { target, bounds: { ...bounds, [field]: field === "w" || field === "h" ? Math.max(1, value) : value, }, }); }; return (
{layer ? ( <> { const value = Number.parseFloat(opacityDraft); if (Number.isFinite(value)) dispatch(commandIds.documentSetLayerOpacity, { layerId: layer.id, opacity: value / 100 }); else setOpacityDraft(String(Math.round(layer.opacity * 100))); }} /> { const value = Number.parseFloat(rotationDraft); if (Number.isFinite(value)) dispatch(commandIds.transformSetRotation, { target, rotation: value * Math.PI / 180 }); else setRotationDraft(rotationDegrees(layerInfo)); }} /> {layer.type !== "group" ? ( ) : null} {locked ? Unlock to edit : null} ) : null}
); } function SimpleInput({ label, suffix, value, disabled, title, onChange, onCommit }: { label: string; suffix: string; value: string; disabled?: boolean; title?: string; onChange: (value: string) => void; onCommit: () => void }) { return ( ); } function rotationDegrees(layerInfo?: IndexedLayerInfo) { return String(Math.round((layerInfo?.layer.transform.rotation ?? 0) * 180 / Math.PI)); } function actionButtonClass() { return "inline-flex h-10 items-center gap-2 rounded-full px-3 text-xs font-semibold text-white/70 transition hover:bg-white/10 hover:text-white disabled:pointer-events-none disabled:opacity-35"; } function BoundsInput({ label, field, draft, disabled, setDraft, commitField, }: { label: string; field: BoundsField; draft: string; disabled?: boolean; setDraft: Dispatch>>; commitField: (field: BoundsField) => void; }) { return ( ); } function draftFromBounds(bounds: Rect): Record { return { x: String(Math.round(bounds.x)), y: String(Math.round(bounds.y)), w: String(Math.round(bounds.w)), h: String(Math.round(bounds.h)), }; }