"use client"; import * as React from "react"; import { cn } from "../../../lib/utils"; import { ControlFieldLabel } from "../../control-layout"; import { Field } from "../../primitives"; import { VectorAxisValue } from "./vector-axis-value"; 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, parseVectorCoordinateDraft, } from "./vector-value"; type VectorPadAxis = "x" | "y"; type VectorPadDragStart = { element: HTMLButtonElement; pointerId: number; clientX: number; clientY: number; value: VectorControlValue; }; const vectorPadAxisLockThresholdPx = 2; export function VectorPadField({ defaultValue, name, onValueChange, padCoordinateMode, padShape = "compact", padVariant = "default", x, xLabel = "X", y, yLabel = "Y", }: VectorControlProps): React.JSX.Element { const [isPointerDragging, setIsPointerDragging] = React.useState(false); const [locks, setLocks] = React.useState({ x: false, y: false }); const fullyLocked = locks.x && locks.y; const normalizedX = formatVectorPadCoordinate(x); const normalizedY = formatVectorPadCoordinate(y); const coordinateMode = padCoordinateMode ?? getDefaultPadCoordinateMode(padVariant); const point = getVectorPoint(normalizedX, normalizedY, coordinateMode); const vectorPadBackgroundImage = vectorPadBackgroundImages[padVariant]; const shiftAxisRef = 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, ) => { const next = { x: locks.x ? x : nextX, y: locks.y ? y : nextY }; if (Number(next.x) === Number(x) && Number(next.y) === Number(y)) return; if (meta) { onValueChange?.(next, meta); return; } onValueChange?.(next); }; const commitCoordinate = (axis: VectorPadAxis, draft: string) => { const coordinate = parseVectorCoordinateDraft(draft); if (coordinate !== null) { updateVector(axis === "x" ? coordinate : x, axis === "y" ? coordinate : y); } }; function getLiveHistoryMeta(): ControlChangeMeta { liveHistoryGroupRef.current ??= createControlHistoryGroupId(`vector:${accessibleName}`); return { history: "merge", historyGroup: liveHistoryGroupRef.current, }; } function updateFromPointer(event: React.PointerEvent): void { const dragStart = dragStartRef.current; if (!dragStart || event.pointerId !== dragStart.pointerId || fullyLocked) return; const nextPoint = pointFromEvent(event, coordinateMode); const nextValue: VectorControlValue = { x: (nextPoint.x * 2 - 1).toFixed(2), y: (nextPoint.y * 2 - 1).toFixed(2), }; if (event.shiftKey && !locks.x && !locks.y) { if (!shiftAxisRef.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; } shiftAxisRef.current = Math.abs(deltaX) >= Math.abs(deltaY) ? "x" : "y"; } updateVector( shiftAxisRef.current === "x" ? nextValue.x : dragStart.value.x, shiftAxisRef.current === "y" ? nextValue.y : dragStart.value.y, getLiveHistoryMeta(), ); return; } shiftAxisRef.current = null; updateVector( nextValue.x, nextValue.y, getLiveHistoryMeta(), ); } function stopPointerDrag(): void { const dragStart = dragStartRef.current; setIsPointerDragging(false); shiftAxisRef.current = null; dragStartRef.current = null; liveHistoryGroupRef.current = null; if (dragStart?.element.hasPointerCapture?.(dragStart.pointerId)) { dragStart.element.releasePointerCapture(dragStart.pointerId); } } function finishPointerDrag(event: React.PointerEvent): void { if (event.pointerId === dragStartRef.current?.pointerId) stopPointerDrag(); } function toggleLock(axis: VectorPadAxis): void { stopPointerDrag(); setLocks(current => ({ ...current, [axis]: !current[axis] })); } return (
{name} commitCoordinate("x", draft)} onToggle={() => toggleLock("x")} value={normalizedX} /> commitCoordinate("y", draft)} onToggle={() => toggleLock("y")} value={normalizedY} />
); }