"use client"; import * as React from "react"; import { cn } from "../../../lib/utils"; import { ControlFieldLabel } from "../../control-layout"; import { EditableSliderValueLabel, Field } from "../../primitives"; import { createControlHistoryGroupId, type ControlChangeMeta, } from "../control-types"; import type { VectorControlProps, VectorControlValue, } from "./vector-control-types"; import { getDefaultPadCoordinateMode, getVectorPoint, pointFromEvent, } from "./vector-pad-geometry"; import { VectorPadGuides, VectorPadHandle } from "./vector-pad-parts"; import { vectorPadBackgroundImages } from "./vector-pad-variants"; import { formatVectorPadCoordinate, getVectorValueLabel, parseVectorValueDraft, } from "./vector-value"; type VectorPadAxisLock = "x" | "y"; type VectorPadDragStart = { clientX: number; clientY: number; value: VectorControlValue; }; const vectorPadAxisLockThresholdPx = 2; export function VectorPadField({ defaultValue, name, onValueChange, padCoordinateMode, padShape = "compact", padVariant = "default", x, y, }: VectorControlProps): React.JSX.Element { const [isPointerDragging, setIsPointerDragging] = React.useState(false); const normalizedX = formatVectorPadCoordinate(x); const normalizedY = formatVectorPadCoordinate(y); const coordinateMode = padCoordinateMode ?? getDefaultPadCoordinateMode(padVariant); const point = getVectorPoint(normalizedX, normalizedY, coordinateMode); const valueLabel = getVectorValueLabel(normalizedX, normalizedY); const vectorPadBackgroundImage = vectorPadBackgroundImages[padVariant]; const axisLockRef = React.useRef(null); const dragStartRef = React.useRef(null); const liveHistoryGroupRef = React.useRef(null); const resetX = formatVectorPadCoordinate(defaultValue?.x); const resetY = formatVectorPadCoordinate(defaultValue?.y); const accessibleName = name || "Vector"; const updateVector = ( nextX: string, nextY: string, meta?: ControlChangeMeta, ) => { if (meta) { onValueChange?.({ x: nextX, y: nextY }, meta); return; } onValueChange?.({ x: nextX, y: nextY }); }; const commitVectorValue = (nextValue: string) => { const nextVector = parseVectorValueDraft(nextValue); if (nextVector) { updateVector(nextVector.x, nextVector.y); } }; function getLiveHistoryMeta(): ControlChangeMeta { liveHistoryGroupRef.current ??= createControlHistoryGroupId(`vector:${accessibleName}`); return { history: "merge", historyGroup: liveHistoryGroupRef.current, }; } function updateFromPointer(event: React.PointerEvent): void { const nextPoint = pointFromEvent(event, coordinateMode); const nextValue: VectorControlValue = { x: (nextPoint.x * 2 - 1).toFixed(2), y: (nextPoint.y * 2 - 1).toFixed(2), }; const dragStart = dragStartRef.current; if (event.shiftKey && dragStart) { if (!axisLockRef.current) { const deltaX = event.clientX - dragStart.clientX; const deltaY = event.clientY - dragStart.clientY; if (Math.hypot(deltaX, deltaY) < vectorPadAxisLockThresholdPx) { updateVector( dragStart.value.x, dragStart.value.y, getLiveHistoryMeta(), ); return; } axisLockRef.current = Math.abs(deltaX) >= Math.abs(deltaY) ? "x" : "y"; } updateVector( axisLockRef.current === "x" ? nextValue.x : dragStart.value.x, axisLockRef.current === "y" ? nextValue.y : dragStart.value.y, getLiveHistoryMeta(), ); return; } axisLockRef.current = null; updateVector( nextValue.x, nextValue.y, getLiveHistoryMeta(), ); } function stopPointerDrag(): void { setIsPointerDragging(false); axisLockRef.current = null; dragStartRef.current = null; liveHistoryGroupRef.current = null; } return ( {name ? (
{name}
) : null}
); }