diff --git a/src-tauri/src/cache_utils.rs b/src-tauri/src/cache_utils.rs index 87a4d50235..f76d04decc 100644 --- a/src-tauri/src/cache_utils.rs +++ b/src-tauri/src/cache_utils.rs @@ -171,13 +171,8 @@ pub fn calculate_patched_warped_hash(adjustments: &serde_json::Value) -> u64 { pub fn calculate_effects_hash(adjustments: &serde_json::Value) -> u64 { let mut hasher = DefaultHasher::new(); - let effects_visible = adjustments - .get("sectionVisibility") - .and_then(|v| v.get("effects")) - .and_then(|s| s.as_bool()) - .unwrap_or(true); - - let blur_enabled = effects_visible && adjustments["lensBlurEnabled"].as_bool().unwrap_or(false); + let blur_enabled = + crate::image_processing::is_effect_enabled(adjustments, "lensBlur", "lensBlurEnabled"); blur_enabled.hash(&mut hasher); if blur_enabled { @@ -198,7 +193,7 @@ pub fn calculate_effects_hash(adjustments: &serde_json::Value) -> u64 { } let relight_enabled = - effects_visible && adjustments["relightEnabled"].as_bool().unwrap_or(false); + crate::image_processing::is_effect_enabled(adjustments, "relight", "relightEnabled"); relight_enabled.hash(&mut hasher); if relight_enabled { @@ -217,7 +212,7 @@ pub fn calculate_effects_hash(adjustments: &serde_json::Value) -> u64 { ); } - let fog_enabled = effects_visible && adjustments["fogEnabled"].as_bool().unwrap_or(false); + let fog_enabled = crate::image_processing::is_effect_enabled(adjustments, "fog", "fogEnabled"); fog_enabled.hash(&mut hasher); if fog_enabled { @@ -293,13 +288,8 @@ pub fn calculate_transform_hash(adjustments: &serde_json::Value) -> u64 { let flip_v = adjustments["flipVertical"].as_bool().unwrap_or(false); flip_v.hash(&mut hasher); - let effects_visible = adjustments - .get("sectionVisibility") - .and_then(|v| v.get("effects")) - .and_then(|s| s.as_bool()) - .unwrap_or(true); - - let blur_enabled = effects_visible && adjustments["lensBlurEnabled"].as_bool().unwrap_or(false); + let blur_enabled = + crate::image_processing::is_effect_enabled(adjustments, "lensBlur", "lensBlurEnabled"); blur_enabled.hash(&mut hasher); if blur_enabled { if let Some(val) = adjustments.get("lensBlurAmount") { @@ -329,7 +319,7 @@ pub fn calculate_transform_hash(adjustments: &serde_json::Value) -> u64 { } let relight_enabled = - effects_visible && adjustments["relightEnabled"].as_bool().unwrap_or(false); + crate::image_processing::is_effect_enabled(adjustments, "relight", "relightEnabled"); relight_enabled.hash(&mut hasher); if relight_enabled { for key in [ @@ -349,7 +339,7 @@ pub fn calculate_transform_hash(adjustments: &serde_json::Value) -> u64 { } } - let fog_enabled = effects_visible && adjustments["fogEnabled"].as_bool().unwrap_or(false); + let fog_enabled = crate::image_processing::is_effect_enabled(adjustments, "fog", "fogEnabled"); fog_enabled.hash(&mut hasher); if fog_enabled { for key in [ diff --git a/src-tauri/src/file_management.rs b/src-tauri/src/file_management.rs index 3946b706e3..dd23fff899 100644 --- a/src-tauri/src/file_management.rs +++ b/src-tauri/src/file_management.rs @@ -2972,23 +2972,22 @@ pub async fn apply_auto_adjustments_to_paths( ); let auto_adjustments_json = auto_results_to_json(&auto_results); + let mut active_tools = + crate::image_processing::active_tools(&existing_metadata.adjustments); + if let (Some(existing_map), Some(auto_map)) = ( existing_metadata.adjustments.as_object_mut(), auto_adjustments_json.as_object(), ) { for (k, v) in auto_map { - if k == "sectionVisibility" { - if let Some(existing_vis_val) = existing_map.get_mut(k) { - if let (Some(existing_vis), Some(auto_vis)) = - (existing_vis_val.as_object_mut(), v.as_object()) - { - for (vis_k, vis_v) in auto_vis { - existing_vis.insert(vis_k.clone(), vis_v.clone()); - } - } - } else { - existing_map.insert(k.clone(), v.clone()); + if k == "activeTools" { + if let Some(auto_tools) = v.as_object() { + active_tools.extend(auto_tools.clone()); } + existing_map.insert( + k.clone(), + serde_json::Value::Object(std::mem::take(&mut active_tools)), + ); } else { existing_map.insert(k.clone(), v.clone()); } diff --git a/src-tauri/src/fog.rs b/src-tauri/src/fog.rs index e8ba44b412..f602834832 100644 --- a/src-tauri/src/fog.rs +++ b/src-tauri/src/fog.rs @@ -76,13 +76,7 @@ pub fn apply_fog<'a>( image: Cow<'a, DynamicImage>, adjustments: &serde_json::Value, ) -> Cow<'a, DynamicImage> { - let effects_visible = adjustments - .get("sectionVisibility") - .and_then(|v| v.get("effects")) - .and_then(|s| s.as_bool()) - .unwrap_or(true); - - if !adjustments["fogEnabled"].as_bool().unwrap_or(false) || !effects_visible { + if !crate::image_processing::is_effect_enabled(adjustments, "fog", "fogEnabled") { return image; } diff --git a/src-tauri/src/image_processing.rs b/src-tauri/src/image_processing.rs index c11c9e90c3..018e54dbca 100644 --- a/src-tauri/src/image_processing.rs +++ b/src-tauri/src/image_processing.rs @@ -2164,6 +2164,68 @@ fn is_section_visible(adjustments: &serde_json::Value, section: &str) -> bool { .unwrap_or(true) } +const LEGACY_SECTION_TOOLS: &[(&str, &[&str])] = &[ + ("basic", &["basic"]), + ("curves", &["curves"]), + ( + "color", + &[ + "whiteBalance", + "colorPresence", + "hue", + "colorGrading", + "colorMixer", + "colorCalibration", + ], + ), + ( + "details", + &[ + "sharpening", + "presence", + "noiseReduction", + "chromaticAberration", + ], + ), + ( + "effects", + &[ + "creative", "lensBlur", "relight", "fog", "lut", "vignette", "grain", + ], + ), +]; + +pub fn is_tool_active(adjustments: &serde_json::Value, tool: &str) -> bool { + match adjustments.get("activeTools") { + Some(active_tools) => active_tools + .get(tool) + .and_then(|active| active.as_bool()) + .unwrap_or(true), + None => LEGACY_SECTION_TOOLS + .iter() + .find(|(_, tools)| tools.contains(&tool)) + .is_none_or(|(section, _)| is_section_visible(adjustments, section)), + } +} + +pub fn active_tools(adjustments: &serde_json::Value) -> serde_json::Map { + match adjustments + .get("activeTools") + .and_then(|tools| tools.as_object()) + { + Some(active_tools) => active_tools.clone(), + None => LEGACY_SECTION_TOOLS + .iter() + .flat_map(|(_, tools)| tools.iter()) + .map(|tool| (tool.to_string(), json!(is_tool_active(adjustments, tool)))) + .collect(), + } +} + +pub fn is_effect_enabled(adjustments: &serde_json::Value, tool: &str, enabled_key: &str) -> bool { + adjustments[enabled_key].as_bool().unwrap_or(false) && is_tool_active(adjustments, tool) +} + fn get_global_adjustments_from_json( js_adjustments: &serde_json::Value, is_raw: bool, @@ -2171,10 +2233,10 @@ fn get_global_adjustments_from_json( baseline_exposure: f32, tonemapper_override: Option, ) -> GlobalAdjustments { - let is_visible = |section: &str| is_section_visible(js_adjustments, section); + let is_visible = |tool: &str| is_tool_active(js_adjustments, tool); - let get_val = |section: &str, key: &str, scale: f32, default: Option| -> f32 { - if is_visible(section) { + let get_val = |tool: &str, key: &str, scale: f32, default: Option| -> f32 { + if is_visible(tool) { js_adjustments[key] .as_f64() .unwrap_or(default.unwrap_or(0.0)) as f32 @@ -2232,6 +2294,9 @@ fn get_global_adjustments_from_json( Vec::new() }; + let color_grading_visible = is_visible("colorGrading"); + let color_mixer_visible = is_visible("colorMixer"); + let cg_obj = js_adjustments .get("colorGrading") .cloned() @@ -2242,7 +2307,7 @@ fn get_global_adjustments_from_json( .cloned() .unwrap_or_default(); - let color_cal_settings = if is_visible("color") { + let color_cal_settings = if is_visible("colorCalibration") { ColorCalibrationSettings { shadows_tint: cal_obj["shadowsTint"].as_f64().unwrap_or(0.0) as f32 / SCALES.color_calibration_hue, @@ -2268,7 +2333,7 @@ fn get_global_adjustments_from_json( let (pipe_to_rendering, rendering_to_pipe) = calculate_agx_matrices(); let rgb_to_lms = white_balance::rgb_to_lms(); - let (has_lut, lut_intensity, lut_is_scene_referred) = if is_visible("effects") { + let (has_lut, lut_intensity, lut_is_scene_referred) = if is_visible("lut") { ( if js_adjustments["lutPath"].is_string() { 1 @@ -2298,69 +2363,69 @@ fn get_global_adjustments_from_json( whites: get_val("basic", "whites", SCALES.whites, None), blacks: get_val("basic", "blacks", SCALES.blacks, None), - saturation: get_val("color", "saturation", SCALES.saturation, None), - vibrance: get_val("color", "vibrance", SCALES.vibrance, None), - hue: get_val("color", "hue", 1.0, None), + saturation: get_val("colorPresence", "saturation", SCALES.saturation, None), + vibrance: get_val("colorPresence", "vibrance", SCALES.vibrance, None), + hue: get_val("hue", "hue", 1.0, None), wb_log_gain_l: white_balance_gains[0], wb_log_gain_m: white_balance_gains[1], wb_log_gain_s: white_balance_gains[2], _pad_color1: 0.0, _pad_color2: 0.0, - sharpness: get_val("details", "sharpness", SCALES.sharpness, None), + sharpness: get_val("sharpening", "sharpness", SCALES.sharpness, None), luma_noise_reduction: get_val( - "details", + "noiseReduction", "lumaNoiseReduction", SCALES.luma_noise_reduction, None, ), color_noise_reduction: get_val( - "details", + "noiseReduction", "colorNoiseReduction", SCALES.color_noise_reduction, None, ), - clarity: get_val("details", "clarity", SCALES.clarity, None), - dehaze: get_val("details", "dehaze", SCALES.dehaze, None), - structure: get_val("details", "structure", SCALES.structure, None), - centré: get_val("details", "centré", SCALES.centré, None), - vignette_amount: get_val("effects", "vignetteAmount", SCALES.vignette_amount, None), + clarity: get_val("presence", "clarity", SCALES.clarity, None), + dehaze: get_val("presence", "dehaze", SCALES.dehaze, None), + structure: get_val("presence", "structure", SCALES.structure, None), + centré: get_val("presence", "centré", SCALES.centré, None), + vignette_amount: get_val("vignette", "vignetteAmount", SCALES.vignette_amount, None), vignette_midpoint: get_val( - "effects", + "vignette", "vignetteMidpoint", SCALES.vignette_midpoint, Some(50.0), ), vignette_roundness: get_val( - "effects", + "vignette", "vignetteRoundness", SCALES.vignette_roundness, Some(0.0), ), vignette_feather: get_val( - "effects", + "vignette", "vignetteFeather", SCALES.vignette_feather, Some(50.0), ), - grain_amount: get_val("effects", "grainAmount", SCALES.grain_amount, None), - grain_size: get_val("effects", "grainSize", SCALES.grain_size, Some(25.0)), + grain_amount: get_val("grain", "grainAmount", SCALES.grain_amount, None), + grain_size: get_val("grain", "grainSize", SCALES.grain_size, Some(25.0)), grain_roughness: get_val( - "effects", + "grain", "grainRoughness", SCALES.grain_roughness, Some(50.0), ), chromatic_aberration_red_cyan: get_val( - "details", + "chromaticAberration", "chromaticAberrationRedCyan", SCALES.chromatic_aberration, None, ), chromatic_aberration_blue_yellow: get_val( - "details", + "chromaticAberration", "chromaticAberrationBlueYellow", SCALES.chromatic_aberration, None, @@ -2395,32 +2460,32 @@ fn get_global_adjustments_from_json( _pad_cg2: 0.0, _pad_cg3: 0.0, _pad_cg4: 0.0, - color_grading_shadows: if is_visible("color") { + color_grading_shadows: if color_grading_visible { parse_color_grade_settings(&cg_obj["shadows"]) } else { ColorGradeSettings::default() }, - color_grading_midtones: if is_visible("color") { + color_grading_midtones: if color_grading_visible { parse_color_grade_settings(&cg_obj["midtones"]) } else { ColorGradeSettings::default() }, - color_grading_highlights: if is_visible("color") { + color_grading_highlights: if color_grading_visible { parse_color_grade_settings(&cg_obj["highlights"]) } else { ColorGradeSettings::default() }, - color_grading_global: if is_visible("color") { + color_grading_global: if color_grading_visible { parse_color_grade_settings(&cg_obj["global"]) } else { ColorGradeSettings::default() }, - color_grading_blending: if is_visible("color") { + color_grading_blending: if color_grading_visible { cg_obj["blending"].as_f64().unwrap_or(50.0) as f32 / SCALES.color_grading_blending } else { 0.5 }, - color_grading_balance: if is_visible("color") { + color_grading_balance: if color_grading_visible { cg_obj["balance"].as_f64().unwrap_or(0.0) as f32 / SCALES.color_grading_balance } else { 0.0 @@ -2430,7 +2495,7 @@ fn get_global_adjustments_from_json( color_calibration: color_cal_settings, - hsl: if is_visible("color") { + hsl: if color_mixer_visible { parse_hsl_adjustments(&js_adjustments.get("hsl").cloned().unwrap_or_default()) } else { [HslColor::default(); 8] @@ -2448,11 +2513,11 @@ fn get_global_adjustments_from_json( _pad_end3: 0.0, _pad_end4: 0.0, - glow_amount: get_val("effects", "glowAmount", SCALES.glow, None), - halation_amount: get_val("effects", "halationAmount", SCALES.halation, None), - flare_amount: get_val("effects", "flareAmount", SCALES.flares, None), + glow_amount: get_val("creative", "glowAmount", SCALES.glow, None), + halation_amount: get_val("creative", "halationAmount", SCALES.halation, None), + flare_amount: get_val("creative", "flareAmount", SCALES.flares, None), sharpness_threshold: get_val( - "details", + "sharpening", "sharpnessThreshold", SCALES.sharpness_threshold, Some(15.0), @@ -2468,10 +2533,10 @@ fn get_mask_adjustments_from_json( return MaskAdjustments::default(); } - let is_visible = |section: &str| is_section_visible(adj, section); + let is_visible = |tool: &str| is_tool_active(adj, tool); - let get_val = |section: &str, key: &str, scale: f32| -> f32 { - if is_visible(section) { + let get_val = |tool: &str, key: &str, scale: f32| -> f32 { + if is_visible(tool) { adj[key].as_f64().unwrap_or(0.0) as f32 / scale } else { 0.0 @@ -2499,8 +2564,11 @@ fn get_mask_adjustments_from_json( } else { Vec::new() }; + let color_grading_visible = is_visible("colorGrading"); + let color_mixer_visible = is_visible("colorMixer"); + let cg_obj = adj.get("colorGrading").cloned().unwrap_or_default(); - let [wb_log_gain_l, wb_log_gain_m, wb_log_gain_s] = if is_visible("color") { + let [wb_log_gain_l, wb_log_gain_m, wb_log_gain_s] = if is_visible("whiteBalance") { white_balance::adaptation_log_gains( global_white_balance, global_white_balance.shifted( @@ -2521,57 +2589,65 @@ fn get_mask_adjustments_from_json( whites: get_val("basic", "whites", SCALES.whites), blacks: get_val("basic", "blacks", SCALES.blacks), - saturation: get_val("color", "saturation", SCALES.saturation), - vibrance: get_val("color", "vibrance", SCALES.vibrance), + saturation: get_val("colorPresence", "saturation", SCALES.saturation), + vibrance: get_val("colorPresence", "vibrance", SCALES.vibrance), - sharpness: get_val("details", "sharpness", SCALES.sharpness), - luma_noise_reduction: get_val("details", "lumaNoiseReduction", SCALES.luma_noise_reduction), + sharpness: get_val("sharpening", "sharpness", SCALES.sharpness), + luma_noise_reduction: get_val( + "noiseReduction", + "lumaNoiseReduction", + SCALES.luma_noise_reduction, + ), color_noise_reduction: get_val( - "details", + "noiseReduction", "colorNoiseReduction", SCALES.color_noise_reduction, ), - clarity: get_val("details", "clarity", SCALES.clarity), - dehaze: get_val("details", "dehaze", SCALES.dehaze), - structure: get_val("details", "structure", SCALES.structure), + clarity: get_val("presence", "clarity", SCALES.clarity), + dehaze: get_val("presence", "dehaze", SCALES.dehaze), + structure: get_val("presence", "structure", SCALES.structure), - glow_amount: get_val("effects", "glowAmount", SCALES.glow), - halation_amount: get_val("effects", "halationAmount", SCALES.halation), - flare_amount: get_val("effects", "flareAmount", SCALES.flares), - sharpness_threshold: get_val("details", "sharpnessThreshold", SCALES.sharpness_threshold), + glow_amount: get_val("creative", "glowAmount", SCALES.glow), + halation_amount: get_val("creative", "halationAmount", SCALES.halation), + flare_amount: get_val("creative", "flareAmount", SCALES.flares), + sharpness_threshold: get_val( + "sharpening", + "sharpnessThreshold", + SCALES.sharpness_threshold, + ), - hue: get_val("color", "hue", 1.0), + hue: get_val("hue", "hue", 1.0), wb_log_gain_l, wb_log_gain_m, wb_log_gain_s, _pad_wb: 0.0, - color_grading_shadows: if is_visible("color") { + color_grading_shadows: if color_grading_visible { parse_color_grade_settings(&cg_obj["shadows"]) } else { ColorGradeSettings::default() }, - color_grading_midtones: if is_visible("color") { + color_grading_midtones: if color_grading_visible { parse_color_grade_settings(&cg_obj["midtones"]) } else { ColorGradeSettings::default() }, - color_grading_highlights: if is_visible("color") { + color_grading_highlights: if color_grading_visible { parse_color_grade_settings(&cg_obj["highlights"]) } else { ColorGradeSettings::default() }, - color_grading_global: if is_visible("color") { + color_grading_global: if color_grading_visible { parse_color_grade_settings(&cg_obj["global"]) } else { ColorGradeSettings::default() }, - color_grading_blending: if is_visible("color") { + color_grading_blending: if color_grading_visible { cg_obj["blending"].as_f64().unwrap_or(50.0) as f32 / SCALES.color_grading_blending } else { 0.5 }, - color_grading_balance: if is_visible("color") { + color_grading_balance: if color_grading_visible { cg_obj["balance"].as_f64().unwrap_or(0.0) as f32 / SCALES.color_grading_balance } else { 0.0 @@ -2579,7 +2655,7 @@ fn get_mask_adjustments_from_json( _pad5: 0.0, _pad6: 0.0, - hsl: if is_visible("color") { + hsl: if color_mixer_visible { parse_hsl_adjustments(&adj.get("hsl").cloned().unwrap_or_default()) } else { [HslColor::default(); 8] @@ -2605,7 +2681,7 @@ pub fn get_all_adjustments_from_json( as_shot: AsShot, render: RenderOptions, ) -> AllAdjustments { - let target_white_balance = if is_section_visible(js_adjustments, "color") { + let target_white_balance = if is_tool_active(js_adjustments, "whiteBalance") { white_balance::from_adjustments(js_adjustments, as_shot.white_balance) } else { as_shot.white_balance @@ -3562,10 +3638,11 @@ pub fn auto_results_to_json(results: &AutoAdjustmentResults) -> serde_json::Valu "centré": results.centre, "dehaze": results.dehaze, - "sectionVisibility": { + "activeTools": { "basic": true, - "color": true, - "effects": true + "colorPresence": true, + "presence": true, + "vignette": true }, "whites": results.whites, "blacks": results.blacks diff --git a/src-tauri/src/lens_blur.rs b/src-tauri/src/lens_blur.rs index d825b59f55..eb10cf55eb 100644 --- a/src-tauri/src/lens_blur.rs +++ b/src-tauri/src/lens_blur.rs @@ -15,13 +15,7 @@ pub fn apply_lens_blur<'a>( image: Cow<'a, DynamicImage>, adjustments: &serde_json::Value, ) -> Cow<'a, DynamicImage> { - let effects_visible = adjustments - .get("sectionVisibility") - .and_then(|v| v.get("effects")) - .and_then(|s| s.as_bool()) - .unwrap_or(true); - - if !adjustments["lensBlurEnabled"].as_bool().unwrap_or(false) || !effects_visible { + if !crate::image_processing::is_effect_enabled(adjustments, "lensBlur", "lensBlurEnabled") { return image; } diff --git a/src-tauri/src/relight.rs b/src-tauri/src/relight.rs index 475be83776..7a584abd34 100644 --- a/src-tauri/src/relight.rs +++ b/src-tauri/src/relight.rs @@ -286,13 +286,7 @@ pub fn apply_relight<'a>( image: Cow<'a, DynamicImage>, adjustments: &serde_json::Value, ) -> Cow<'a, DynamicImage> { - let effects_visible = adjustments - .get("sectionVisibility") - .and_then(|v| v.get("effects")) - .and_then(|s| s.as_bool()) - .unwrap_or(true); - - if !adjustments["relightEnabled"].as_bool().unwrap_or(false) || !effects_visible { + if !crate::image_processing::is_effect_enabled(adjustments, "relight", "relightEnabled") { return image; } diff --git a/src/components/adjustments/AdjustmentSubSection.tsx b/src/components/adjustments/AdjustmentSubSection.tsx index 5f2cf80bda..97c1934ad2 100644 --- a/src/components/adjustments/AdjustmentSubSection.tsx +++ b/src/components/adjustments/AdjustmentSubSection.tsx @@ -3,9 +3,11 @@ import { ChevronDown } from 'lucide-react'; import clsx from 'clsx'; import { useShallow } from 'zustand/react/shallow'; import Text from '../ui/Text'; +import VisibilityToggle, { HIDDEN_CONTENT_CLASS } from '../ui/VisibilityToggle'; import { TextVariants } from '../../types/typography'; import { useSettingsStore } from '../../store/useSettingsStore'; import { useCollapsibleHeight } from '../../hooks/useCollapsibleHeight'; +import { useToolVisibility } from '../../context/ToolVisibilityContext'; import { ADJUSTMENT_SECTION_TOOLS, getAdjustmentSectionToolIds, withAdjustmentLayout } from '../../utils/adjustments'; interface AdjustmentSubSectionProps { @@ -17,6 +19,7 @@ interface AdjustmentSubSectionProps { } export default function AdjustmentSubSection({ actions, children, id, order, title }: AdjustmentSubSectionProps) { + const visibility = useToolVisibility(id); const { appSettings, handleSettingsChange } = useSettingsStore( useShallow((state) => ({ appSettings: state.appSettings, @@ -48,10 +51,15 @@ export default function AdjustmentSubSection({ actions, children, id, order, tit return (
-
- +
+ {title} + {visibility && ( +
+ +
+ )} {actions &&
e.stopPropagation()}>{actions}
}
-
+
{children}
diff --git a/src/components/adjustments/Effects.tsx b/src/components/adjustments/Effects.tsx index 734f35dc33..9cd570c6a6 100644 --- a/src/components/adjustments/Effects.tsx +++ b/src/components/adjustments/Effects.tsx @@ -1,12 +1,13 @@ -import { useState, useEffect, useRef, useMemo } from 'react'; +import { useState, useEffect, useRef, useMemo, ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { invoke } from '@tauri-apps/api/core'; import { toast } from 'react-toastify'; import { Loader2, Circle, Hexagon, Octagon, Aperture, Plus, X } from 'lucide-react'; import { motion } from 'framer-motion'; import clsx from 'clsx'; -import Slider from '../ui/Slider'; +import Slider, { SliderChangeEvent } from '../ui/Slider'; import Switch from '../ui/Switch'; +import VisibilityToggle, { HIDDEN_CONTENT_CLASS } from '../ui/VisibilityToggle'; import { Adjustments, Effect, @@ -29,11 +30,12 @@ import { DepthRangePicker } from '../ui/DepthRangePicker'; import { useProcessStore } from '../../store/useProcessStore'; import { useSettingsStore } from '../../store/useSettingsStore'; import { useEditorStore } from '../../store/useEditorStore'; +import { useToolVisibility } from '../../context/ToolVisibilityContext'; interface EffectsPanelProps { adjustments: Adjustments; isForMask?: boolean; - setAdjustments(adjustments: Partial | ((prev: Adjustments) => Adjustments)): any; + setAdjustments(adjustments: Partial | ((prev: Adjustments) => Adjustments)): void; handleLutSelect(path: string, isSceneReferred: boolean): void; onLutHover?: (path: string | null) => void; appSettings: AppSettings | null; @@ -151,6 +153,46 @@ const BokehShapeSwitch = ({ selectedShape, onShapeChange }: BokehShapeSwitchProp ); }; +interface SpatialEffectProps { + children: ReactNode; + enabled: boolean; + id: string; + label: string; + onToggle(enabled: boolean): void; +} + +const SpatialEffect = ({ children, enabled, id, label, onToggle }: SpatialEffectProps) => { + const visibility = useToolVisibility(id, 'spatial'); + + return ( +
+
+ + {enabled && visibility && ( + + )} +
+ +
+
+
+ {children} +
+
+
+
+ ); +}; + export default function EffectsPanel({ adjustments, setAdjustments, @@ -178,7 +220,7 @@ export default function EffectsPanel({ ...prev, lensBlurDepthMap: b64, })); - } catch (e: any) { + } catch (e) { toast.error(`Failed to generate depth map: ${e}`); setAdjustments((prev: Partial) => ({ ...prev, lensBlurEnabled: false })); } finally { @@ -194,7 +236,7 @@ export default function EffectsPanel({ ...prev, relightNormalMap: b64, })); - } catch (e: any) { + } catch (e) { toast.error(`Failed to generate normal map: ${e}`); setAdjustments((prev: Partial) => ({ ...prev, relightEnabled: false })); } finally { @@ -210,7 +252,7 @@ export default function EffectsPanel({ ...prev, fogDepthMap: b64, })); - } catch (e: any) { + } catch (e) { toast.error(`Failed to generate depth map: ${e}`); setAdjustments((prev: Partial) => ({ ...prev, fogEnabled: false })); } finally { @@ -218,8 +260,8 @@ export default function EffectsPanel({ } }; - const handleAdjustmentChange = (key: string, value: any) => { - const numericValue = typeof value === 'boolean' ? value : parseInt(value, 10); + const handleAdjustmentChange = (key: string, value: boolean | number | string) => { + const numericValue = typeof value === 'boolean' ? value : parseInt(String(value), 10); setAdjustments((prev: Partial) => ({ ...prev, [key]: numericValue })); }; @@ -292,6 +334,9 @@ export default function EffectsPanel({ })); }; + const handleLightSliderChange = (key: keyof RelightLight) => (e: SliderChangeEvent) => + handleLightChange(key, parseInt(String(e.target.value), 10)); + const lightColorField = useParsedTextField( activeLight?.color ?? '#ffffff', (color) => handleLightChange('color', color), @@ -324,7 +369,7 @@ export default function EffectsPanel({ label={t('adjustments.effects.glow')} max={100} min={0} - onChange={(e: any) => handleAdjustmentChange(CreativeAdjustment.GlowAmount, e.target.value)} + onChange={(e: SliderChangeEvent) => handleAdjustmentChange(CreativeAdjustment.GlowAmount, e.target.value)} step={1} value={adjustments.glowAmount} onDragStateChange={onDragStateChange} @@ -334,7 +379,9 @@ export default function EffectsPanel({ label={t('adjustments.effects.halation')} max={100} min={0} - onChange={(e: any) => handleAdjustmentChange(CreativeAdjustment.HalationAmount, e.target.value)} + onChange={(e: SliderChangeEvent) => + handleAdjustmentChange(CreativeAdjustment.HalationAmount, e.target.value) + } step={1} value={adjustments.halationAmount} onDragStateChange={onDragStateChange} @@ -345,7 +392,9 @@ export default function EffectsPanel({ label={t('adjustments.effects.lightFlares')} max={100} min={0} - onChange={(e: any) => handleAdjustmentChange(CreativeAdjustment.FlareAmount, e.target.value)} + onChange={(e: SliderChangeEvent) => + handleAdjustmentChange(CreativeAdjustment.FlareAmount, e.target.value) + } step={1} value={adjustments.flareAmount} onDragStateChange={onDragStateChange} @@ -363,524 +412,499 @@ export default function EffectsPanel({ title={t('adjustments.effects.spatial')} >
-
- - -
-
-
- {isGeneratingDepth ? ( -
-
- - - {aiModelDownloadStatus - ? t('editor.masks.settings.aiModelDownloading') - : t('editor.ai.generatingDepthMap')} - -
- {aiModelDownloadStatus && ( - - {aiModelDownloadStatus} - - )} -
- ) : ( - <> - handleAdjustmentChange(Effect.LensBlurAmount, e.target.value)} - step={1} - value={adjustments.lensBlurAmount ?? 50} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - handleAdjustmentChange(Effect.lensBlurDiffusion, e.target.value)} - step={1} - value={adjustments.lensBlurDiffusion ?? 0} - onDragStateChange={onDragStateChange} - /> - - - setAdjustments((prev: Partial) => ({ - ...prev, - [Effect.LensBlurShape]: shapeId, - })) - } - /> - - { - setAdjustments((prev: Partial) => ({ - ...prev, - lensBlurMinDepth: 100 - values.maxDepth, - lensBlurMaxDepth: 100 - values.minDepth, - lensBlurMinFade: values.maxFade, - lensBlurMaxFade: values.minFade, - })); - }} - onDragStateChange={onDragStateChange} - /> - - )} + + {isGeneratingDepth ? ( +
+
+ + + {aiModelDownloadStatus + ? t('editor.masks.settings.aiModelDownloading') + : t('editor.ai.generatingDepthMap')} +
+ {aiModelDownloadStatus && ( + + {aiModelDownloadStatus} + + )}
-
-
-
- - -
-
-
- {isGeneratingNormals ? ( -
-
- - - {aiModelDownloadStatus - ? t('editor.masks.settings.aiModelDownloading') - : t('editor.ai.generatingNormalMap')} - -
- {aiModelDownloadStatus && ( - - {aiModelDownloadStatus} - - )} + ) : ( + <> + + handleAdjustmentChange(Effect.LensBlurAmount, e.target.value) + } + step={1} + value={adjustments.lensBlurAmount ?? 50} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + + + handleAdjustmentChange(Effect.lensBlurDiffusion, e.target.value) + } + step={1} + value={adjustments.lensBlurDiffusion ?? 0} + onDragStateChange={onDragStateChange} + /> + + + setAdjustments((prev: Partial) => ({ + ...prev, + [Effect.LensBlurShape]: shapeId, + })) + } + /> + + { + setAdjustments((prev: Partial) => ({ + ...prev, + lensBlurMinDepth: 100 - values.maxDepth, + lensBlurMaxDepth: 100 - values.minDepth, + lensBlurMinFade: values.maxFade, + lensBlurMaxFade: values.minFade, + })); + }} + onDragStateChange={onDragStateChange} + /> + + )} + + +
+ {isGeneratingNormals ? ( +
+
+ + + {aiModelDownloadStatus + ? t('editor.masks.settings.aiModelDownloading') + : t('editor.ai.generatingNormalMap')} + +
+ {aiModelDownloadStatus && ( + + {aiModelDownloadStatus} + + )} +
+ ) : ( + <> + + handleAdjustmentChange(Effect.RelightAmbient, e.target.value) + } + step={1} + value={adjustments.relightAmbient ?? 0} + onDragStateChange={onDragStateChange} + /> + + + handleAdjustmentChange(Effect.RelightSoftness, e.target.value) + } + step={1} + value={adjustments.relightSoftness ?? 25} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + + + handleAdjustmentChange(Effect.RelightShine, e.target.value) + } + step={1} + value={adjustments.relightShine ?? 0} + onDragStateChange={onDragStateChange} + /> + + handleAdjustmentChange(Effect.RelightShadows, enabled)} + /> + + {adjustments.relightShadows && ( + + handleAdjustmentChange(Effect.RelightShadowSoftness, e.target.value) + } + step={1} + value={adjustments.relightShadowSoftness ?? 15} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + )} + +
+
+ {relightLights.map((light, index) => ( +
setHoveredLightId(light.id)} + onMouseLeave={() => setHoveredLightId(null)} + > + + {hoveredLightId === light.id && ( + + )} +
+ ))} +
- ) : ( - <> - handleAdjustmentChange(Effect.RelightAmbient, e.target.value)} - step={1} - value={adjustments.relightAmbient ?? 0} - onDragStateChange={onDragStateChange} - /> - - handleAdjustmentChange(Effect.RelightSoftness, e.target.value)} - step={1} - value={adjustments.relightSoftness ?? 25} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - handleAdjustmentChange(Effect.RelightShine, e.target.value)} - step={1} - value={adjustments.relightShine ?? 0} - onDragStateChange={onDragStateChange} - /> - - handleAdjustmentChange(Effect.RelightShadows, enabled)} - /> - - {adjustments.relightShadows && ( + + {activeLight ? ( + <> +
+ + {t('adjustments.effects.relightType')} + + handleLightChange('type', val)} + /> +
+ + handleLightChange('color', color)} + /> + - handleAdjustmentChange(Effect.RelightShadowSoftness, e.target.value) - } + defaultValue={60} + onChange={handleLightSliderChange('intensity')} step={1} - value={adjustments.relightShadowSoftness ?? 15} + value={activeLight.intensity} onDragStateChange={onDragStateChange} fillOrigin="min" /> - )} - -
-
- {relightLights.map((light, index) => ( -
setHoveredLightId(light.id)} - onMouseLeave={() => setHoveredLightId(null)} - > - - {hoveredLightId === light.id && ( - - )} -
- ))} - -
- {activeLight ? ( + {activeLight.type !== 'directional' && ( <> -
- - {t('adjustments.effects.relightType')} - - handleLightChange('type', val)} - /> -
- - handleLightChange('color', color)} + handleLightChange('intensity', parseInt(e.target.value, 10))} + defaultValue={30} + onChange={handleLightSliderChange('radius')} + step={1} + value={activeLight.radius} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + + )} + + {activeLight.type !== 'point' && ( + <> + - {activeLight.type !== 'directional' && ( - <> - handleLightChange('depth', parseInt(e.target.value, 10))} - step={1} - value={activeLight.depth} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - handleLightChange('radius', parseInt(e.target.value, 10))} - step={1} - value={activeLight.radius} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - )} - - {activeLight.type !== 'point' && ( - <> - handleLightChange('angle', parseInt(e.target.value, 10))} - step={1} - suffix="°" - value={activeLight.angle} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - - handleLightChange('elevation', parseInt(e.target.value, 10)) - } - step={1} - suffix="°" - value={activeLight.elevation} - onDragStateChange={onDragStateChange} - /> - - )} - - {activeLight.type === 'spot' && ( - <> - handleLightChange('cone', parseInt(e.target.value, 10))} - step={1} - value={activeLight.cone} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - - handleLightChange('feather', parseInt(e.target.value, 10)) - } - step={1} - value={activeLight.feather} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - )} + + + )} + {activeLight.type === 'spot' && ( + <> - handleLightChange('temperature', parseInt(e.target.value, 10)) - } + min={0} + defaultValue={40} + onChange={handleLightSliderChange('cone')} step={1} - value={activeLight.temperature} - trackClassName="temperature-gradient-track" + value={activeLight.cone} onDragStateChange={onDragStateChange} + fillOrigin="min" /> handleLightChange('tint', parseInt(e.target.value, 10))} + min={0} + defaultValue={50} + onChange={handleLightSliderChange('feather')} step={1} - value={activeLight.tint} - trackClassName="tint-gradient-track" + value={activeLight.feather} onDragStateChange={onDragStateChange} + fillOrigin="min" /> - ) : ( - - {t('adjustments.effects.relightHint')} - )} -
- - )} -
-
+ + + + + + ) : ( + + {t('adjustments.effects.relightHint')} + + )} +
+ + )}
-
-
- - -
-
-
- {isGeneratingFogDepth ? ( -
-
- - - {aiModelDownloadStatus - ? t('editor.masks.settings.aiModelDownloading') - : t('editor.ai.generatingDepthMap')} - -
- {aiModelDownloadStatus && ( - - {aiModelDownloadStatus} - - )} -
- ) : ( - <> - handleAdjustmentChange(Effect.FogAmount, e.target.value)} - step={1} - value={adjustments.fogAmount ?? 50} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - handleAdjustmentChange(Effect.FogStart, e.target.value)} - step={1} - value={adjustments.fogStart ?? 0} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - handleAdjustmentChange(Effect.FogDensity, e.target.value)} - step={1} - value={adjustments.fogDensity ?? 50} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - handleAdjustmentChange(Effect.FogHeight, e.target.value)} - step={1} - value={adjustments.fogHeight ?? 0} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - handleAdjustmentChange(Effect.FogVariation, e.target.value)} - step={1} - value={adjustments.fogVariation ?? 25} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - handleAdjustmentChange(Effect.FogGlow, e.target.value)} - step={1} - value={adjustments.fogGlow ?? 25} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - handleAdjustmentChange(Effect.FogTemperature, e.target.value)} - step={1} - value={adjustments.fogTemperature ?? 0} - trackClassName="temperature-gradient-track" - onDragStateChange={onDragStateChange} - /> - - handleAdjustmentChange(Effect.FogTint, e.target.value)} - step={1} - value={adjustments.fogTint ?? 0} - trackClassName="tint-gradient-track" - onDragStateChange={onDragStateChange} - /> - - )} + + + {isGeneratingFogDepth ? ( +
+
+ + + {aiModelDownloadStatus + ? t('editor.masks.settings.aiModelDownloading') + : t('editor.ai.generatingDepthMap')} +
+ {aiModelDownloadStatus && ( + + {aiModelDownloadStatus} + + )}
-
-
+ ) : ( + <> + handleAdjustmentChange(Effect.FogAmount, e.target.value)} + step={1} + value={adjustments.fogAmount ?? 50} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + + handleAdjustmentChange(Effect.FogStart, e.target.value)} + step={1} + value={adjustments.fogStart ?? 0} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + + handleAdjustmentChange(Effect.FogDensity, e.target.value)} + step={1} + value={adjustments.fogDensity ?? 50} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + + handleAdjustmentChange(Effect.FogHeight, e.target.value)} + step={1} + value={adjustments.fogHeight ?? 0} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + + handleAdjustmentChange(Effect.FogVariation, e.target.value)} + step={1} + value={adjustments.fogVariation ?? 25} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + + handleAdjustmentChange(Effect.FogGlow, e.target.value)} + step={1} + value={adjustments.fogGlow ?? 25} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + + + handleAdjustmentChange(Effect.FogTemperature, e.target.value) + } + step={1} + value={adjustments.fogTemperature ?? 0} + trackClassName="temperature-gradient-track" + onDragStateChange={onDragStateChange} + /> + + handleAdjustmentChange(Effect.FogTint, e.target.value)} + step={1} + value={adjustments.fogTint ?? 0} + trackClassName="tint-gradient-track" + onDragStateChange={onDragStateChange} + /> + + )} +
)} @@ -910,7 +934,7 @@ export default function EffectsPanel({ label={t('adjustments.effects.amount')} max={100} min={-100} - onChange={(e: any) => handleAdjustmentChange(Effect.VignetteAmount, e.target.value)} + onChange={(e: SliderChangeEvent) => handleAdjustmentChange(Effect.VignetteAmount, e.target.value)} step={1} value={adjustments.vignetteAmount} onDragStateChange={onDragStateChange} @@ -920,7 +944,7 @@ export default function EffectsPanel({ label={t('adjustments.effects.midpoint')} max={100} min={0} - onChange={(e: any) => handleAdjustmentChange(Effect.VignetteMidpoint, e.target.value)} + onChange={(e: SliderChangeEvent) => handleAdjustmentChange(Effect.VignetteMidpoint, e.target.value)} step={1} value={adjustments.vignetteMidpoint} onDragStateChange={onDragStateChange} @@ -930,7 +954,7 @@ export default function EffectsPanel({ label={t('adjustments.effects.roundness')} max={100} min={-100} - onChange={(e: any) => handleAdjustmentChange(Effect.VignetteRoundness, e.target.value)} + onChange={(e: SliderChangeEvent) => handleAdjustmentChange(Effect.VignetteRoundness, e.target.value)} step={1} value={adjustments.vignetteRoundness} onDragStateChange={onDragStateChange} @@ -940,7 +964,7 @@ export default function EffectsPanel({ label={t('adjustments.effects.feather')} max={100} min={0} - onChange={(e: any) => handleAdjustmentChange(Effect.VignetteFeather, e.target.value)} + onChange={(e: SliderChangeEvent) => handleAdjustmentChange(Effect.VignetteFeather, e.target.value)} step={1} value={adjustments.vignetteFeather} onDragStateChange={onDragStateChange} @@ -955,7 +979,7 @@ export default function EffectsPanel({ label={t('adjustments.effects.amount')} max={100} min={0} - onChange={(e: any) => handleAdjustmentChange(Effect.GrainAmount, e.target.value)} + onChange={(e: SliderChangeEvent) => handleAdjustmentChange(Effect.GrainAmount, e.target.value)} step={1} value={adjustments.grainAmount} onDragStateChange={onDragStateChange} @@ -965,7 +989,7 @@ export default function EffectsPanel({ label={t('adjustments.effects.size')} max={100} min={0} - onChange={(e: any) => handleAdjustmentChange(Effect.GrainSize, e.target.value)} + onChange={(e: SliderChangeEvent) => handleAdjustmentChange(Effect.GrainSize, e.target.value)} step={1} value={adjustments.grainSize} onDragStateChange={onDragStateChange} @@ -976,7 +1000,7 @@ export default function EffectsPanel({ label={t('adjustments.effects.roughness')} max={100} min={0} - onChange={(e: any) => handleAdjustmentChange(Effect.GrainRoughness, e.target.value)} + onChange={(e: SliderChangeEvent) => handleAdjustmentChange(Effect.GrainRoughness, e.target.value)} step={1} value={adjustments.grainRoughness} onDragStateChange={onDragStateChange} diff --git a/src/components/panel/editor/EditorToolbar.tsx b/src/components/panel/editor/EditorToolbar.tsx index 1743d869d0..fa1609e01f 100644 --- a/src/components/panel/editor/EditorToolbar.tsx +++ b/src/components/panel/editor/EditorToolbar.tsx @@ -222,7 +222,7 @@ const EditorToolbar = memo( colorCalibration: 'Color Calibration', toneMapper: 'Tone Mapper', showClipping: 'Show Clipping', - sectionVisibility: 'Section Visibility', + activeTools: 'Tool Visibility', flareAmount: 'Flare Amount', glowAmount: 'Glow Amount', halationAmount: 'Halation Amount', diff --git a/src/components/panel/right/AdjustmentSectionsSubMenu.tsx b/src/components/panel/right/AdjustmentSectionsSubMenu.tsx index dea73fdcc5..a703e5a7dd 100644 --- a/src/components/panel/right/AdjustmentSectionsSubMenu.tsx +++ b/src/components/panel/right/AdjustmentSectionsSubMenu.tsx @@ -9,7 +9,7 @@ import { AdjustmentLayout } from '../../ui/AppProperties'; import { useSettingsStore } from '../../../store/useSettingsStore'; import { ADJUSTMENT_SECTIONS, - ADJUSTMENT_SECTION_TOOLS, + ADJUSTMENT_TOOLS_BY_ID, AdjustmentSectionTool, DEFAULT_HIDDEN_ADJUSTMENT_TOOLS, getAdjustmentSectionOrder, @@ -52,9 +52,6 @@ interface SectionRowProps { toolOrder: string[]; } -const ALL_TOOLS = Object.values(ADJUSTMENT_SECTION_TOOLS).flat(); -const TOOLS_BY_ID = Object.fromEntries(ALL_TOOLS.map((tool) => [tool.id, tool])); - const toggleId = (ids: string[], id: string) => (ids.includes(id) ? ids.filter((item) => item !== id) : [...ids, id]); function useReorderableList(savedOrder: string[], onCommit: (order: string[]) => void) { @@ -187,7 +184,7 @@ function SectionRow({ key={tool} onDragEnd={tools.handleDragEnd} onToggle={() => onToggleTool(tool)} - tool={TOOLS_BY_ID[tool]} + tool={ADJUSTMENT_TOOLS_BY_ID[tool]} /> ))} diff --git a/src/components/panel/right/ControlsPanel.tsx b/src/components/panel/right/ControlsPanel.tsx index 1678afd482..385bf1a654 100644 --- a/src/components/panel/right/ControlsPanel.tsx +++ b/src/components/panel/right/ControlsPanel.tsx @@ -14,12 +14,16 @@ import Resizer from '../../ui/Resizer'; import AdjustmentSectionsSubMenu from './AdjustmentSectionsSubMenu'; import { Adjustments, - SectionVisibility, INITIAL_ADJUSTMENTS, ADJUSTMENT_SECTIONS, + getActiveTools, getVisibleAdjustmentSections, + isToolActive, + setToolActive, + toggleToolActive, } from '../../../utils/adjustments'; import { useContextMenu } from '../../../context/ContextMenuContext'; +import { ToolVisibilityContext } from '../../../context/ToolVisibilityContext'; import { OPTION_SEPARATOR, Orientation } from '../../ui/AppProperties'; import Text from '../../ui/Text'; import { TextVariants, TextColors, TextWeights } from '../../../types/typography'; @@ -102,17 +106,11 @@ export default function Controls() { [setUI], ); - const handleToggleVisibility = (sectionName: string) => { - setAdjustments((prev: Adjustments) => { - const currentVisibility: SectionVisibility = prev.sectionVisibility || INITIAL_ADJUSTMENTS.sectionVisibility; - return { - ...prev, - sectionVisibility: { - ...currentVisibility, - [sectionName]: !currentVisibility[sectionName], - }, - }; - }); + const handleToggleVisibility = (id: string) => { + setAdjustments((prev: Adjustments) => ({ + ...prev, + activeTools: toggleToolActive(getActiveTools(prev), id), + })); }; const handleResetAdjustments = () => { @@ -124,7 +122,7 @@ export default function Controls() { acc[key] = INITIAL_ADJUSTMENTS[key as keyof Adjustments]; return acc; }, {}), - sectionVisibility: { ...INITIAL_ADJUSTMENTS.sectionVisibility }, + activeTools: { ...INITIAL_ADJUSTMENTS.activeTools }, })); }; @@ -169,10 +167,7 @@ export default function Controls() { setAdjustments((prev: Adjustments) => ({ ...prev, ...copiedSectionAdjustments.values, - sectionVisibility: { - ...(prev.sectionVisibility || INITIAL_ADJUSTMENTS.sectionVisibility), - [sectionName]: true, - }, + activeTools: setToolActive(getActiveTools(prev), sectionName, true), })); }; @@ -184,10 +179,7 @@ export default function Controls() { setAdjustments((prev: Adjustments) => ({ ...prev, ...resetValues, - sectionVisibility: { - ...(prev.sectionVisibility || INITIAL_ADJUSTMENTS.sectionVisibility), - [sectionName]: true, - }, + activeTools: setToolActive(getActiveTools(prev), sectionName, true), })); }; @@ -298,31 +290,35 @@ export default function Controls() { }[sectionName]; const title = t(`editor.adjustments.sections.${sectionName}`); - const sectionVisibility = adjustments.sectionVisibility || INITIAL_ADJUSTMENTS.sectionVisibility; + const activeTools = getActiveTools(adjustments); return (
handleSectionContextMenu(e, sectionName)} onToggle={() => handleToggleSection(sectionName)} onToggleVisibility={() => handleToggleVisibility(sectionName)} title={title} > - + + +
); diff --git a/src/components/panel/right/MasksPanel.tsx b/src/components/panel/right/MasksPanel.tsx index 5a0d03fff5..83d942de71 100644 --- a/src/components/panel/right/MasksPanel.tsx +++ b/src/components/panel/right/MasksPanel.tsx @@ -79,8 +79,14 @@ import { INITIAL_MASK_CONTAINER, MaskContainer, ADJUSTMENT_SECTIONS, + getActiveTools, getVisibleAdjustmentSections, + isToolActive, + mergeActiveTools, + setToolActive, + toggleToolActive, } from '../../../utils/adjustments'; +import { ToolVisibilityContext } from '../../../context/ToolVisibilityContext'; import { useContextMenu } from '../../../context/ContextMenuContext'; import { OPTION_SEPARATOR, Orientation, Panel } from '../../ui/AppProperties'; import { createSubMask } from '../../../utils/maskUtils'; @@ -1408,7 +1414,10 @@ function ContainerRow({ label: item.name || item.preset.name, onClick: () => { const newAdj = { ...container.adjustments, ...(item.adjustments || item.preset.adjustments) }; - newAdj.sectionVisibility = { ...container.adjustments.sectionVisibility, ...newAdj.sectionVisibility }; + newAdj.activeTools = mergeActiveTools( + container.adjustments, + item.adjustments || item.preset.adjustments, + ); updateContainer(container.id, { adjustments: newAdj }); }, }; @@ -1925,10 +1934,7 @@ function SettingsPanel({ const newMaskAdjustments = { ...currentAdjustments, ...presetAdjustments, - sectionVisibility: { - ...(currentAdjustments.sectionVisibility || INITIAL_MASK_ADJUSTMENTS.sectionVisibility), - ...(presetAdjustments.sectionVisibility || {}), - }, + activeTools: mergeActiveTools(currentAdjustments, presetAdjustments), }; updateContainer(container.id, { adjustments: newMaskAdjustments }); }; @@ -2018,12 +2024,11 @@ function SettingsPanel({ }); }; - const handleToggleVisibility = (sectionName: string) => { + const handleToggleVisibility = (id: string) => { if (!isActive) return; const cur = container.adjustments; - const vis = cur.sectionVisibility || INITIAL_MASK_ADJUSTMENTS.sectionVisibility; updateContainer(container.id, { - adjustments: { ...cur, sectionVisibility: { ...vis, [sectionName]: !vis[sectionName] } }, + adjustments: { ...cur, activeTools: toggleToolActive(getActiveTools(cur), id) }, }); }; @@ -2051,10 +2056,7 @@ function SettingsPanel({ setMaskContainerAdjustments((prev: any) => ({ ...prev, ...copiedSectionAdjustments.values, - sectionVisibility: { - ...(prev.sectionVisibility || INITIAL_MASK_ADJUSTMENTS.sectionVisibility), - [sectionName]: true, - }, + activeTools: setToolActive(getActiveTools(prev), sectionName, true), })); }; @@ -2068,10 +2070,7 @@ function SettingsPanel({ setMaskContainerAdjustments((prev: any) => ({ ...prev, ...resetValues, - sectionVisibility: { - ...(prev.sectionVisibility || INITIAL_MASK_ADJUSTMENTS.sectionVisibility), - [sectionName]: true, - }, + activeTools: setToolActive(getActiveTools(prev), sectionName, true), })); }; @@ -2104,8 +2103,7 @@ function SettingsPanel({ ]); }; - const sectionVisibility = - displayContainer.adjustments.sectionVisibility || INITIAL_MASK_ADJUSTMENTS.sectionVisibility; + const activeTools = getActiveTools(displayContainer.adjustments); const visibleSections = getVisibleAdjustmentSections(appSettings?.adjustmentLayout); return ( @@ -2272,19 +2270,23 @@ function SettingsPanel({ key={sectionName} title={title} isOpen={collapsibleState[sectionName]} - isContentVisible={sectionVisibility[sectionName]} + isContentVisible={isToolActive(activeTools, sectionName)} onToggle={() => handleToggleSection(sectionName)} onToggleVisibility={() => handleToggleVisibility(sectionName)} onContextMenu={(e: any) => handleSectionContextMenu(e, sectionName)} > - + + + ); })} diff --git a/src/components/ui/CollapsibleSection.tsx b/src/components/ui/CollapsibleSection.tsx index 3296621e37..e16b950f6e 100644 --- a/src/components/ui/CollapsibleSection.tsx +++ b/src/components/ui/CollapsibleSection.tsx @@ -1,19 +1,19 @@ -import { useRef, useState } from 'react'; -import { ChevronDown, Eye, EyeOff } from 'lucide-react'; +import { MouseEvent, ReactNode, useRef, useState } from 'react'; +import { ChevronDown } from 'lucide-react'; import clsx from 'clsx'; -import { useTranslation } from 'react-i18next'; import Text from './Text'; +import VisibilityToggle, { HIDDEN_CONTENT_CLASS } from './VisibilityToggle'; import { TextVariants, TextWeights } from '../../types/typography'; import { useCollapsibleHeight } from '../../hooks/useCollapsibleHeight'; interface CollapsibleSectionProps { canToggleVisibility?: boolean; - children: any; + children: ReactNode; isContentVisible: boolean; isOpen: boolean; - onContextMenu?: any; - onToggle: any; - onToggleVisibility?: any; + onContextMenu?(event: MouseEvent): void; + onToggle(): void; + onToggleVisibility?(): void; title: string; } @@ -27,10 +27,9 @@ export default function CollapsibleSection({ onToggleVisibility = () => {}, title, }: CollapsibleSectionProps) { - const { t } = useTranslation(); const { contentRef, wrapperRef } = useCollapsibleHeight(isOpen); const [isHovering, setIsHovering] = useState(false); - const hoverTimeoutRef = useRef(null); + const hoverTimeoutRef = useRef | null>(null); const handleMouseEnter = () => { if (!canToggleVisibility) { @@ -49,11 +48,6 @@ export default function CollapsibleSection({ setIsHovering(false); }; - const handleVisibilityClick = (e: any) => { - e.stopPropagation(); - onToggleVisibility(); - }; - return (
{canToggleVisibility && (
- + isVisible={isContentVisible} + onToggle={onToggleVisibility} + />
)}
@@ -92,10 +80,7 @@ export default function CollapsibleSection({
{children} diff --git a/src/components/ui/VisibilityToggle.tsx b/src/components/ui/VisibilityToggle.tsx new file mode 100644 index 0000000000..660e09286d --- /dev/null +++ b/src/components/ui/VisibilityToggle.tsx @@ -0,0 +1,35 @@ +import { Eye, EyeOff } from 'lucide-react'; +import clsx from 'clsx'; +import { useTranslation } from 'react-i18next'; + +export const HIDDEN_CONTENT_CLASS = 'opacity-30 pointer-events-none'; + +interface VisibilityToggleProps { + className?: string; + isVisible: boolean; + onToggle(): void; + revealOnHover?: boolean; +} + +export default function VisibilityToggle({ className, isVisible, onToggle, revealOnHover }: VisibilityToggleProps) { + const { t } = useTranslation(); + + return ( + + ); +} diff --git a/src/context/ToolVisibilityContext.ts b/src/context/ToolVisibilityContext.ts new file mode 100644 index 0000000000..a4ede3c8e6 --- /dev/null +++ b/src/context/ToolVisibilityContext.ts @@ -0,0 +1,23 @@ +import { createContext, useContext } from 'react'; +import { ActiveTools, isToolActive } from '../utils/adjustments'; + +interface ToolVisibilityContextValue { + activeTools: ActiveTools; + onToggleTool(id: string): void; + section: string; +} + +export const ToolVisibilityContext = createContext(null); + +export const useToolVisibility = (id: string, parentId?: string) => { + const context = useContext(ToolVisibilityContext); + if (!context) { + return null; + } + const isVisible = isToolActive(context.activeTools, id); + return { + isDimmed: !isVisible && isToolActive(context.activeTools, parentId ?? context.section), + isVisible, + toggle: () => context.onToggleTool(id), + }; +}; diff --git a/src/hooks/useEditorActions.ts b/src/hooks/useEditorActions.ts index 3f33322ff4..27a0bfa91a 100644 --- a/src/hooks/useEditorActions.ts +++ b/src/hooks/useEditorActions.ts @@ -12,7 +12,10 @@ import { COPYABLE_ADJUSTMENT_KEYS, PasteMode, LensAdjustment, + getActiveTools, + mergeActiveTools, normalizeLoadedAdjustments, + setToolActive, } from '../utils/adjustments'; import { calculateCenteredCrop } from '../utils/cropUtils'; import { Invokes } from '../components/ui/AppProperties'; @@ -80,7 +83,7 @@ export function useEditorActions() { setAdjustments((prev: Adjustments) => ({ ...prev, ...autoAdjustments, - sectionVisibility: { ...prev.sectionVisibility, ...autoAdjustments.sectionVisibility }, + activeTools: mergeActiveTools(prev, autoAdjustments), })); } catch (err) { toast.error(`Failed to apply auto adjustments: ${err}`); @@ -147,10 +150,7 @@ export function useEditorActions() { lutSize: result.size, lutIntensity: 100, lutIsSceneReferred: isBuiltIn, - sectionVisibility: { - ...(prev.sectionVisibility || INITIAL_ADJUSTMENTS.sectionVisibility), - effects: true, - }, + activeTools: setToolActive(getActiveTools(prev), 'lut', true), })); } catch (err) { toast.error(`Failed to load LUT: ${err}`); diff --git a/src/utils/adjustments.ts b/src/utils/adjustments.ts index ab9b33bc5d..cc6017d336 100644 --- a/src/utils/adjustments.ts +++ b/src/utils/adjustments.ts @@ -240,6 +240,7 @@ export interface ParametricCurve { } export interface Adjustments { + activeTools: ActiveTools; [index: string]: any; aiPatches: Array; aspectRatio: number | null; @@ -331,7 +332,6 @@ export interface Adjustments { orientationSteps: number; rotation: number; saturation: number; - sectionVisibility: SectionVisibility; shadows: number; sharpness: number; sharpnessThreshold: number; @@ -415,6 +415,7 @@ interface Hsl { } export interface MaskAdjustments { + activeTools: ActiveTools; [index: string]: any; blacks: number; brightness: number; @@ -437,7 +438,6 @@ export interface MaskAdjustments { id?: string; lumaNoiseReduction: number; saturation: number; - sectionVisibility: SectionVisibility; shadows: number; sharpness: number; sharpnessThreshold: number; @@ -467,14 +467,7 @@ export interface Sections { effects: Array; } -export interface SectionVisibility { - [index: string]: boolean; - basic: boolean; - curves: boolean; - color: boolean; - details: boolean; - effects: boolean; -} +export type ActiveTools = Record; export const COLOR_LABELS: Array = [ { name: 'red', color: '#ef4444' }, @@ -544,6 +537,7 @@ export const getDefaultCurves = (): Curves => ({ export const DEFAULT_PARAMETRIC_CURVE = getDefaultParametricCurve(); export const INITIAL_MASK_ADJUSTMENTS: MaskAdjustments = { + activeTools: {}, blacks: 0, brightness: 0, clarity: 0, @@ -573,13 +567,6 @@ export const INITIAL_MASK_ADJUSTMENTS: MaskAdjustments = { hue: 0, lumaNoiseReduction: 0, saturation: 0, - sectionVisibility: { - basic: true, - curves: true, - color: true, - details: true, - effects: true, - }, shadows: 0, sharpness: 0, sharpnessThreshold: 15, @@ -600,6 +587,7 @@ export const INITIAL_MASK_CONTAINER: MaskContainer = { }; export const INITIAL_ADJUSTMENTS: Adjustments = { + activeTools: {}, aiPatches: [], aspectRatio: null, baselineExposure: true, @@ -689,13 +677,6 @@ export const INITIAL_ADJUSTMENTS: Adjustments = { orientationSteps: 0, rotation: 0, saturation: 0, - sectionVisibility: { - basic: true, - curves: true, - color: true, - details: true, - effects: true, - }, shadows: 0, sharpness: 0, sharpnessThreshold: 15, @@ -787,10 +768,8 @@ export const normalizeLoadedAdjustments = (loadedAdjustments: Adjustments): any ? deepCloneParametric(containerAdjustments.parametricCurve) : getDefaultParametricCurve(), curveMode: containerAdjustments.curveMode || INITIAL_MASK_ADJUSTMENTS.curveMode, - sectionVisibility: { - ...INITIAL_MASK_ADJUSTMENTS.sectionVisibility, - ...(containerAdjustments.sectionVisibility || {}), - }, + activeTools: getActiveTools(containerAdjustments), + sectionVisibility: undefined, sharpnessThreshold: containerAdjustments.sharpnessThreshold ?? INITIAL_MASK_ADJUSTMENTS.sharpnessThreshold, }, subMasks: normalizedSubMasks, @@ -885,10 +864,8 @@ export const normalizeLoadedAdjustments = (loadedAdjustments: Adjustments): any curveMode: loadedAdjustments.curveMode || INITIAL_ADJUSTMENTS.curveMode, masks: normalizedMasks, aiPatches: normalizedAiPatches, - sectionVisibility: { - ...INITIAL_ADJUSTMENTS.sectionVisibility, - ...(loadedAdjustments.sectionVisibility || {}), - }, + activeTools: getActiveTools(loadedAdjustments), + sectionVisibility: undefined, sharpnessThreshold: loadedAdjustments.sharpnessThreshold ?? INITIAL_ADJUSTMENTS.sharpnessThreshold, }; }; @@ -1129,6 +1106,7 @@ export const withAdjustmentLayout = (settings: AppSettings, changes: Partial; } export const ADJUSTMENT_SECTION_TOOLS: Record> = { @@ -1148,15 +1126,58 @@ export const ADJUSTMENT_SECTION_TOOLS: Record = Object.fromEntries( + ALL_ADJUSTMENT_TOOLS.map((tool) => [tool.id, tool]), +); + export const getAdjustmentSectionToolIds = (section: string): string[] => (ADJUSTMENT_SECTION_TOOLS[section] ?? []).map((tool) => tool.id); +const getToolIds = (id: string): Array => { + const expand = (tool: AdjustmentSectionTool) => tool.subTools ?? [tool.id]; + const sectionTools = ADJUSTMENT_SECTION_TOOLS[id]; + if (sectionTools) { + return sectionTools.flatMap(expand); + } + const tool = ADJUSTMENT_TOOLS_BY_ID[id]; + return tool ? expand(tool) : [id]; +}; + +export const isToolActive = (activeTools: ActiveTools, id: string): boolean => + getToolIds(id).some((tool) => activeTools[tool] !== false); + +export const setToolActive = (activeTools: ActiveTools, id: string, active: boolean): ActiveTools => ({ + ...activeTools, + ...Object.fromEntries(getToolIds(id).map((tool) => [tool, active])), +}); + +export const toggleToolActive = (activeTools: ActiveTools, id: string): ActiveTools => + setToolActive(activeTools, id, !isToolActive(activeTools, id)); + +interface ToolStateSource { + activeTools?: ActiveTools; + sectionVisibility?: Record; +} + +export const getActiveTools = (adjustments: ToolStateSource): ActiveTools => + adjustments.activeTools ?? + Object.entries(adjustments.sectionVisibility ?? {}) + .filter(([, visible]) => !visible) + .reduce((activeTools, [id]) => setToolActive(activeTools, id, false), {}); + +export const mergeActiveTools = (base: ToolStateSource, overrides: ToolStateSource): ActiveTools => ({ + ...getActiveTools(base), + ...getActiveTools(overrides), +}); + export const getAdjustmentToolOrder = (section: string, toolOrder?: Record): string[] => reconcileOrder(getAdjustmentSectionToolIds(section), toolOrder?.[section]);