"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import { Handle, Position, type Node, type NodeProps } from "@xyflow/react"; import { useMutation } from "convex/react"; import { Sun } from "lucide-react"; import { api } from "@/convex/_generated/api"; import type { Id } from "@/convex/_generated/dataModel"; import { useAuthQuery } from "@/hooks/use-auth-query"; import { useDebouncedCallback } from "@/hooks/use-debounced-callback"; import { useCanvasSync } from "@/components/canvas/canvas-sync-context"; import BaseNodeWrapper from "@/components/canvas/nodes/base-node-wrapper"; import AdjustmentPreview from "@/components/canvas/nodes/adjustment-preview"; import { ParameterSlider, type SliderConfig, type SliderValue, } from "@/src/components/tool-ui/parameter-slider"; import { cloneAdjustmentData, DEFAULT_LIGHT_ADJUST_DATA, normalizeLightAdjustData, type LightAdjustData, } from "@/lib/image-pipeline/adjustment-types"; import { LIGHT_PRESETS } from "@/lib/image-pipeline/presets"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { toast } from "@/lib/toast"; type LightAdjustNodeData = LightAdjustData & { _status?: string; _statusMessage?: string; }; export type LightAdjustNodeType = Node; type PresetDoc = { _id: Id<"adjustmentPresets">; name: string; params: unknown; }; export default function LightAdjustNode({ id, data, selected, width }: NodeProps) { const { queueNodeDataUpdate } = useCanvasSync(); const savePreset = useMutation(api.presets.save); const userPresets = (useAuthQuery(api.presets.list, { nodeType: "light-adjust" }) ?? []) as PresetDoc[]; const [localData, setLocalData] = useState(() => normalizeLightAdjustData({ ...cloneAdjustmentData(DEFAULT_LIGHT_ADJUST_DATA), ...data }), ); const [presetSelection, setPresetSelection] = useState("custom"); const localDataRef = useRef(localData); useEffect(() => { localDataRef.current = localData; }, [localData]); useEffect(() => { const timer = window.setTimeout(() => { setLocalData( normalizeLightAdjustData({ ...cloneAdjustmentData(DEFAULT_LIGHT_ADJUST_DATA), ...data }), ); }, 0); return () => { window.clearTimeout(timer); }; }, [data]); const queueSave = useDebouncedCallback(() => { void queueNodeDataUpdate({ nodeId: id as Id<"nodes">, data: localDataRef.current, }); }, 16); const updateData = (updater: (draft: LightAdjustData) => LightAdjustData) => { setPresetSelection("custom"); setLocalData((current) => { const next = updater(current); localDataRef.current = next; queueSave(); return next; }); }; const builtinOptions = useMemo(() => Object.entries(LIGHT_PRESETS), []); const sliderConfigs = useMemo( () => [ { id: "brightness", label: "Brightness", min: -100, max: 100, value: DEFAULT_LIGHT_ADJUST_DATA.brightness, }, { id: "contrast", label: "Contrast", min: -100, max: 100, value: DEFAULT_LIGHT_ADJUST_DATA.contrast, }, { id: "exposure", label: "Exposure", min: -5, max: 5, step: 0.01, precision: 2, value: DEFAULT_LIGHT_ADJUST_DATA.exposure, }, { id: "highlights", label: "Highlights", min: -100, max: 100, value: DEFAULT_LIGHT_ADJUST_DATA.highlights, }, { id: "shadows", label: "Shadows", min: -100, max: 100, value: DEFAULT_LIGHT_ADJUST_DATA.shadows, }, { id: "whites", label: "Whites", min: -100, max: 100, value: DEFAULT_LIGHT_ADJUST_DATA.whites, }, { id: "blacks", label: "Blacks", min: -100, max: 100, value: DEFAULT_LIGHT_ADJUST_DATA.blacks, }, { id: "vignette-amount", label: "Vignette", min: 0, max: 1, step: 0.01, precision: 2, value: DEFAULT_LIGHT_ADJUST_DATA.vignette.amount, }, ], [], ); const sliderValues = useMemo( () => [ { id: "brightness", value: localData.brightness }, { id: "contrast", value: localData.contrast }, { id: "exposure", value: localData.exposure }, { id: "highlights", value: localData.highlights }, { id: "shadows", value: localData.shadows }, { id: "whites", value: localData.whites }, { id: "blacks", value: localData.blacks }, { id: "vignette-amount", value: localData.vignette.amount }, ], [ localData.blacks, localData.brightness, localData.contrast, localData.exposure, localData.highlights, localData.shadows, localData.vignette.amount, localData.whites, ], ); const applyPresetValue = (value: string) => { if (value === "custom") { setPresetSelection("custom"); return; } if (value.startsWith("builtin:")) { const key = value.replace("builtin:", ""); const preset = LIGHT_PRESETS[key]; if (!preset) return; const next = cloneAdjustmentData(preset); setPresetSelection(value); setLocalData(next); localDataRef.current = next; queueSave(); return; } if (value.startsWith("user:")) { const presetId = value.replace("user:", "") as Id<"adjustmentPresets">; const preset = userPresets.find((entry) => entry._id === presetId); if (!preset) return; const next = normalizeLightAdjustData(preset.params); setPresetSelection(value); setLocalData(next); localDataRef.current = next; queueSave(); } }; const handleSavePreset = async () => { const name = window.prompt("Preset-Name"); if (!name) return; await savePreset({ name, nodeType: "light-adjust", params: localData, }); toast.success("Preset gespeichert"); }; return (
Licht
{ const valueById = new Map(values.map((entry) => [entry.id, entry.value])); updateData((current) => ({ ...current, brightness: valueById.get("brightness") ?? current.brightness, contrast: valueById.get("contrast") ?? current.contrast, exposure: valueById.get("exposure") ?? current.exposure, highlights: valueById.get("highlights") ?? current.highlights, shadows: valueById.get("shadows") ?? current.shadows, whites: valueById.get("whites") ?? current.whites, blacks: valueById.get("blacks") ?? current.blacks, vignette: { ...current.vignette, amount: valueById.get("vignette-amount") ?? current.vignette.amount, }, preset: null, })); }} onAction={async (actionId) => { if (actionId === "apply") { queueSave(); } }} />
); }