diff --git a/Lux/lux_react/src/components/AudioControlPanel.jsx b/Lux/lux_react/src/components/AudioControlPanel.jsx index bc109a22..06985a7f 100644 --- a/Lux/lux_react/src/components/AudioControlPanel.jsx +++ b/Lux/lux_react/src/components/AudioControlPanel.jsx @@ -16,7 +16,19 @@ const AudioControlPanel = ({ return 'prompt'; }; - const formatValue = (value) => Math.round((value || 0) * 100); + + // Advanced mixer frequency bands + const frequencyBands = [ + { id: 'volume', name: 'Volume', range: 'Overall', color: '#666', value: audioFeatures?.volume || 0 }, + { id: 'bass', name: 'Bass', range: '60-250Hz', color: '#ff3366', value: audioFeatures?.bassLevel || 0 }, + { id: 'mid', name: 'Mid', range: '500-2kHz', color: '#ffaa33', value: audioFeatures?.midLevel || 0 }, + { id: 'high', name: 'High', range: '2k+Hz', color: '#66ff33', value: audioFeatures?.highLevel || 0 } + ]; + + const linearToDb = (linear) => { + if (linear <= 0) return -60; + return 20 * Math.log10(linear); + }; return (
@@ -52,85 +64,56 @@ const AudioControlPanel = ({ {isEnabled && ( <> -
-
-
- Bass - {formatValue(audioFeatures?.bassLevel)}% -
-
-
-
-
- -
-
- Mid - {formatValue(audioFeatures?.midLevel)}% -
-
-
-
-
- -
-
- High - {formatValue(audioFeatures?.highLevel)}% -
-
-
-
-
- -
-
- Volume - {formatValue(audioFeatures?.volume)}% -
-
-
-
-
-
- -
-
+ {/* Vertical Mixer Strips - Primary Control */} +
+
+

πŸŽ›οΈ Audio Mixer

- Beat Detection -
-
- {audioFeatures?.beatDetected ? '🎡 Beat!' : '⏸️ Waiting'} + {audioFeatures?.beatDetected ? '🎡 Beat!' : '⏸️ Waiting'}
- -
-
-
0.01 ? 'active' : ''}`} /> - Audio Input -
-
- {(audioFeatures?.volume || 0) > 0.01 ? 'πŸ”Š Detected' : 'πŸ”‡ Silent'} -
+ +
+ {frequencyBands.map(band => ( +
+
+ + {band.name} + + {band.range} +
+ +
+
+
+ 0 + -12 + -24 + -36 + -60 +
+
+ +
+
+ {linearToDb(band.value).toFixed(1)}dB +
+
+
+ ))}
- πŸŽ›οΈ Sensitivity + πŸŽ›οΈ Master Sensitivity {Math.round((sensitivity ?? 1.0) * 100)}%
diff --git a/Lux/lux_react/src/components/AudioMixer.css b/Lux/lux_react/src/components/AudioMixer.css index 76651880..6d3e3fc2 100644 --- a/Lux/lux_react/src/components/AudioMixer.css +++ b/Lux/lux_react/src/components/AudioMixer.css @@ -8,9 +8,8 @@ color: #ffffff; font-family: 'Inter', 'Arial', sans-serif; width: 100%; - overflow: hidden; - max-height: 60vh; - overflow-y: auto; + overflow: visible; + max-height: none; } /* Header */ @@ -175,13 +174,10 @@ /* Frequency Bands */ .frequency-bands { margin-bottom: 16px; -} - -.frequency-bands h4 { - margin-bottom: 15px; - color: #fff; - font-size: 1.2rem; - text-align: center; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 12px; + padding: 16px; } .bands-grid { @@ -491,7 +487,8 @@ .audio-mixer { padding: 12px; margin: 0; - max-height: 50vh; + max-height: none; + overflow: visible; } .mixer-header h3 { @@ -552,7 +549,8 @@ @media (max-width: 480px) { .audio-mixer { padding: 8px; - max-height: 40vh; + max-height: none; + overflow: visible; } .bands-grid { diff --git a/Lux/lux_react/src/components/ControlPanel.jsx b/Lux/lux_react/src/components/ControlPanel.jsx index 79678d07..39db322b 100644 --- a/Lux/lux_react/src/components/ControlPanel.jsx +++ b/Lux/lux_react/src/components/ControlPanel.jsx @@ -11,13 +11,12 @@ import { useJenModule } from '../hooks/useJenModule.js'; // Our simple, reliable import MediaController from "./MediaController"; import TabNavigation from "./TabNavigation"; import HomePane from "./panes/HomePane"; -import SourceImagePane from "./panes/SourceImagePane"; -import TargetImagePane from "./panes/TargetImagePane"; import BrushPane from "./panes/BrushPane"; import AudioPane from "./panes/AudioPane"; import {SceneChooserPane} from "./panes/SceneChooserPane"; import {PaneContext} from "./panes/PaneContext.jsx"; import RealtimeCamera from "./RealtimeCamera.jsx"; +import ImagePane from "./panes/ImagePane.jsx"; function ControlPanel({ dimensions, panelSize, activePane, onPaneChange }) { const { sliderValues, onSliderChange } = React.useContext(ControlPanelContext); @@ -244,14 +243,20 @@ function ControlPanel({ dimensions, panelSize, activePane, onPaneChange }) { ); - case "source": + case "image": return ( - + ); + case "source": case "target": - return ; + // Redirect old pane values to the new unified image pane + return ( + + + + ); case "brush": return ; case "audio": diff --git a/Lux/lux_react/src/components/MediaController.jsx b/Lux/lux_react/src/components/MediaController.jsx index 57f94eff..3b406855 100644 --- a/Lux/lux_react/src/components/MediaController.jsx +++ b/Lux/lux_react/src/components/MediaController.jsx @@ -128,7 +128,6 @@ function MediaController({ isOverlay = false }) { const syncWithBackend = () => { if (window.module && typeof window.module.get_animation_running === 'function') { const backendRunning = window.module.get_animation_running(); - console.log('[MediaController] Syncing with backend animation state:', backendRunning); setIsRunning(backendRunning); } }; diff --git a/Lux/lux_react/src/components/TabNavigation.jsx b/Lux/lux_react/src/components/TabNavigation.jsx index 509f7ddb..0c401e7e 100644 --- a/Lux/lux_react/src/components/TabNavigation.jsx +++ b/Lux/lux_react/src/components/TabNavigation.jsx @@ -82,7 +82,7 @@ function TabNavigation({ activePane, onPaneChange }) { } - value="source" + value="image" sx={{ minHeight: '50px', '&.Mui-selected': { @@ -90,8 +90,8 @@ function TabNavigation({ activePane, onPaneChange }) { background: 'rgba(25, 118, 210, 0.08)' } }} - aria-label="Source Image" - title="Source Image" + aria-label="Image Controls" + title="Image Controls" /> - } - value="target" - sx={{ - minHeight: '50px', - '&.Mui-selected': { - color: 'primary.main', - background: 'rgba(25, 118, 210, 0.08)' - } - }} - aria-label="Target Image" - title="Target Image" - /> - } value="brush" diff --git a/Lux/lux_react/src/components/panes/AudioPane.css b/Lux/lux_react/src/components/panes/AudioPane.css index 27a6e6e4..739ed870 100644 --- a/Lux/lux_react/src/components/panes/AudioPane.css +++ b/Lux/lux_react/src/components/panes/AudioPane.css @@ -9,8 +9,10 @@ color: #ffffff; font-family: 'Inter', 'Arial', sans-serif; font-weight: 500; - overflow: hidden; + overflow: visible; transition: all 0.3s ease; + min-height: auto; + max-height: none; } .audio-pane.party-mode { @@ -169,79 +171,18 @@ gap: 24px; } -/* Mixer Presets Container */ -.mixer-presets { +/* Audio Control Center - Flat Layout */ +.audio-control-center { background: rgba(255, 255, 255, 0.05); border-radius: 16px; padding: 20px; border: 1px solid rgba(255, 255, 255, 0.1); + overflow: visible; + max-height: none; + height: auto; } -/* Frequency Bands Section */ -.frequency-bands { - background: rgba(255, 255, 255, 0.08); - border-radius: 12px; - padding: 12px; - margin-bottom: 16px; - border: 1px solid rgba(255, 255, 255, 0.15); - transition: all 0.3s ease; -} - -.frequency-bands h4 { - margin: 0 0 8px 0; - font-weight: 700; - font-size: 14px; - color: #fff; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); -} - -/* Master Sensitivity Section */ -.master-sensitivity { - background: rgba(255, 255, 255, 0.08); - border-radius: 12px; - padding: 12px; - margin-bottom: 16px; - border: 1px solid rgba(255, 255, 255, 0.15); - transition: all 0.3s ease; -} - -.master-sensitivity h4 { - margin: 0 0 8px 0; - font-weight: 700; - font-size: 14px; - color: #fff; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); -} - -/* Collapsed state - minimal space */ -.frequency-bands:has(.collapsible-header) { - padding: 8px 12px; - margin-bottom: 8px; -} - -.master-sensitivity:has(.collapsible-header) { - padding: 8px 12px; - margin-bottom: 8px; -} - -/* When sections are collapsed, reduce their visual footprint */ -.frequency-bands:not(:has(.simple-meters:not([style*="display: none"]))) { - min-height: auto; -} - -.master-sensitivity:not(:has(.sensitivity-content:not([style*="display: none"]))) { - min-height: auto; -} - -/* Advanced Mixer Section */ -.advanced-mixer-section { - background: rgba(255, 255, 255, 0.08); - border-radius: 12px; - padding: 16px; - border: 1px solid rgba(255, 255, 255, 0.15); -} - -.advanced-mixer-section h4 { +.audio-control-center h4 { margin: 0 0 12px 0; font-weight: 700; font-size: 14px; @@ -417,11 +358,12 @@ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); } -/* Simple Meters */ -.simple-meters { +/* Simple Meters - Flat Style */ +.audio-control-center .simple-meters { display: flex; flex-direction: column; gap: 16px; + margin: 16px 0; } .meter-row { @@ -484,16 +426,14 @@ font-weight: 600; } -/* Beat Section */ -.beat-section { +/* Beat Section - Flat Style */ +.audio-control-center .beat-section { display: flex; justify-content: space-between; align-items: center; - padding: 12px 16px; - background: rgba(255, 255, 255, 0.08); - border-radius: 8px; - margin-top: 8px; - border: 1px solid rgba(255, 255, 255, 0.15); + padding: 12px 0; + margin: 16px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .beat-indicator { @@ -623,7 +563,7 @@ .feature-list .feature-item { padding: 8px 12px; - background: rgba(255, 255, 255, 0.1); + background: rgba(228, 163, 163, 0.1); border-radius: 6px; font-size: 14px; color: #fff; @@ -634,18 +574,28 @@ /* Mobile Responsiveness */ @media (max-width: 768px) { .audio-pane { - margin: 4px; + margin: 2px; padding: 12px; + overflow: visible; + max-height: none; + } + + .audio-control-center { + padding: 16px; + overflow: visible; + max-height: none; } .header-main { flex-direction: column; + margin-bottom: 16px; } .audio-toggle, .party-toggle { min-width: unset; width: 100%; + padding: 12px 16px; } .sensitivity-header { @@ -656,6 +606,7 @@ .meter-row { flex-direction: column; + gap: 8px; } .meter-group { @@ -666,6 +617,7 @@ flex-direction: column; gap: 8px; text-align: center; + padding: 8px 0; } .party-features { @@ -675,32 +627,97 @@ .feature-list { grid-template-columns: 1fr; } + + /* Ensure mixer content is scrollable */ + .mixer-content { + overflow: visible; + max-height: none; + } + + /* Reduce spacing in collapsible sections */ + .collapsible-header { + padding: 8px 4px; + margin-bottom: 8px; + } + + .autoplay-controls, + .party-mode-info { + margin: 8px 0; + padding: 12px; + } } @media (max-width: 480px) { .audio-pane { - margin: 2px; + margin: 1px; padding: 8px; + overflow: visible; + max-height: none; + } + + .audio-control-center { + padding: 12px; + overflow: visible; + max-height: none; + } + + .header-main { + gap: 8px; + margin-bottom: 12px; + } + + .audio-toggle, + .party-toggle { + padding: 10px 12px; + font-size: 14px; } .viz-header { flex-direction: column; gap: 8px; align-items: stretch; + margin-bottom: 12px; } .mixer-toggle { width: 100%; + padding: 8px; + font-size: 14px; + } + + /* Further reduce spacing for very small screens */ + .collapsible-header { + padding: 6px 2px; + margin-bottom: 6px; + } + + .collapsible-header h4 { + font-size: 13px; + } + + .autoplay-controls, + .party-mode-info { + margin: 6px 0; + padding: 10px; + } + + .simple-meters { + margin: 12px 0; + gap: 12px; + } + + .meter-row { + gap: 6px; } } -/* Birthday Party Mode Styles */ -.party-mode-info { +/* Birthday Party Mode Styles - Flat */ +.audio-control-center .party-mode-info { background: linear-gradient(135deg, rgba(255, 107, 53, 0.1) 0%, rgba(255, 138, 80, 0.1) 100%); border: 1px solid rgba(255, 107, 53, 0.3); - border-radius: 12px; - padding: 16px; - margin-top: 12px; + border-radius: 8px; + padding: 12px; + margin: 12px 0; } .party-mode-info .party-header h4 { @@ -796,13 +813,13 @@ line-height: 1.4; } -/* Scene-Agnostic Autoplay Styles */ -.autoplay-controls { +/* Scene-Agnostic Autoplay Styles - Flat */ +.audio-control-center .autoplay-controls { background: rgba(156, 39, 176, 0.05); border: 1px solid rgba(156, 39, 176, 0.2); - border-radius: 12px; - padding: 16px; - margin-top: 12px; + border-radius: 8px; + padding: 12px; + margin: 12px 0; } .autoplay-status p { diff --git a/Lux/lux_react/src/components/panes/AudioPane.jsx b/Lux/lux_react/src/components/panes/AudioPane.jsx index 5b8c741f..a75bcaf7 100644 --- a/Lux/lux_react/src/components/panes/AudioPane.jsx +++ b/Lux/lux_react/src/components/panes/AudioPane.jsx @@ -2,9 +2,6 @@ import React, { useState, useCallback, useEffect } from 'react'; import { useAudioContext } from '../AudioContext'; import AudioMixer from '../AudioMixer'; import './AudioPane.css'; -import { Accordion, AccordionSummary, AccordionDetails } from '@mui/material'; -import { ExpandMore } from '@mui/icons-material'; -import { Typography, Box, Button } from '@mui/material'; import { HiMicrophone } from 'react-icons/hi'; import { FaMicrophoneSlash } from "react-icons/fa6"; @@ -21,16 +18,11 @@ const AudioPane = () => { handleMixerGainChange } = useAudioContext(); - // Party mode state const [partyMode, setPartyMode] = useState(true); // Default ON for birthday celebration - const [mixerExpanded, setMixerExpanded] = useState(false); - const [showAdvanced, setShowAdvanced] = useState(false); + const [mixerExpanded, setMixerExpanded] = useState(true); // Default expanded since mixer is primary control - // Collapsible sections for advanced view const [showFrequencyBands, setShowFrequencyBands] = useState(false); - const [showMasterSensitivity, setShowMasterSensitivity] = useState(false); const [showMixerPresets, setShowMixerPresets] = useState(true); - const [showIntegrationStatus, setShowIntegrationStatus] = useState(false); // Integration status state const [integrationStatus, setIntegrationStatus] = useState(null); @@ -91,11 +83,6 @@ const AudioPane = () => { // The useEffect will handle sensitivity changes based on the new party mode state }; - const handleSensitivityChange = useCallback((value) => { - console.log(`πŸŽ›οΈ Sensitivity changed to: ${value}%`); - setSensitivity(value / 100); // Convert percentage to decimal - }, [setSensitivity]); - const handleFrequencyBandConfig = useCallback((config) => { console.log('πŸŽ›οΈ Frequency band config updated:', config); if (config.gains && handleMixerGainChange) { @@ -112,10 +99,6 @@ const AudioPane = () => { console.log('πŸŽ›οΈ Mixer state changed:', mixerState); }, []); - // Convert sensitivity from decimal to percentage for display - const sensitivityPercentage = Math.round(sensitivity * 100); - const maxSensitivity = 200; // 0-200% range for all modes - // Enhanced integration status check const checkIntegrationStatus = useCallback(() => { if (!window.module) return; @@ -140,8 +123,6 @@ const AudioPane = () => { } }, []); - // Autoplay control handlers - const [autoplayIntensity, setAutoplayIntensity] = useState(0.002); const handleToggleAutoplay = useCallback(() => { if (!window.module || !window.module.enable_scene_autoplay) return; @@ -160,22 +141,6 @@ const AudioPane = () => { } }, [integrationStatus?.autoplay_active, checkIntegrationStatus]); - const handleIntensityChange = useCallback((e) => { - const intensity = parseFloat(e.target.value); - setAutoplayIntensity(intensity); - - if (window.module && window.module.set_autoplay_intensity) { - try { - const success = window.module.set_autoplay_intensity(intensity); - if (success) { - console.log(`🎲 Autoplay intensity set to: ${intensity}`); - } - } catch (error) { - console.error('🎲 ❌ Error setting autoplay intensity:', error); - } - } - }, []); - return (
{/* Mobile-First Header */} @@ -190,149 +155,79 @@ const AudioPane = () => { {isEnabled ? : } - -
{/* Main Controls */}
- {/* Mixer Presets Container - Reorganized */} + {/* Audio Control Center - Flat Layout */} {isEnabled && ( -
-
-

πŸŽ›οΈ Audio Control Center

- -
- - {/* Live Audio Frequency Bands - Top of mixer */} -
- {mixerExpanded ? ( -
setShowFrequencyBands(!showFrequencyBands)}> -

🎡 Live Audio Frequency Bands

- {showFrequencyBands ? 'β–²' : 'β–Ό'} -
- ) : ( +
+ {/* Live Audio Frequency Bands */} + {mixerExpanded ? ( +
setShowFrequencyBands(!showFrequencyBands)}>

🎡 Live Audio Frequency Bands

- )} - {(mixerExpanded ? showFrequencyBands : true) && ( -
-
-
- -
-
-
- {Math.round(mixerState.volume || 0)}% + {showFrequencyBands ? 'β–²' : 'β–Ό'} +
+ ) : ( +

🎡 Live Audio Frequency Bands

+ )} + {(mixerExpanded ? showFrequencyBands : true) && ( +
+
+
+ +
+
+ {Math.round(mixerState.volume || 0)}%
+
-
-
- -
-
-
- {Math.round(mixerState.bass || 0)}% -
- -
- -
-
-
- {Math.round(mixerState.mid || 0)}% -
- -
- -
-
-
- {Math.round(mixerState.high || 0)}% +
+
+ +
+
+ {Math.round(mixerState.bass || 0)}%
- - {/* Beat Indicator */} -
-
- πŸ₯ - BEAT -
- -
- - {mixerState.isProcessing ? '🎡 Processing' : '⏸️ Quiet'} - + +
+ +
+
-
-
- )} -
- - {/* Master Sensitivity - Middle of mixer */} -
- {(mixerExpanded ? showMasterSensitivity : true) && ( -
-
- - {partyMode && ( -
- πŸŽ‚ Perfect for Jen's Birthday! 🎈 -
- )} + {Math.round(mixerState.mid || 0)}%
-
- handleSensitivityChange(parseInt(e.target.value))} - className={`sensitivity-slider ${partyMode ? 'party-slider' : ''}`} - /> -
- 0% - 50% - 100% - {maxSensitivity}% +
+ +
+
+ {Math.round(mixerState.high || 0)}%
- )} -
+
+ )} + - {/* Advanced Mixer Presets - Bottom of mixer */} + {/* Mixer Presets */} {mixerExpanded && ( -
+ <>
setShowMixerPresets(!showMixerPresets)}>

πŸŽ›οΈ Mixer Presets

{showMixerPresets ? 'β–²' : 'β–Ό'} @@ -349,79 +244,9 @@ const AudioPane = () => { setSensitivity={setSensitivity} partyMode={partyMode} /> - - {/* Birthday Party Mode Info - Only in advanced view */} - {partyMode && ( -
-
-

πŸŽ‚ Birthday Party Mode Active!

-

Enhanced sensitivity and celebration effects enabled

-
-
- )}
)} - - {/* Scene-Agnostic Autoplay Controls */} -
-
setShowIntegrationStatus(!showIntegrationStatus)}> -

🎲 Scene Autoplay

- {showIntegrationStatus ? 'β–²' : 'β–Ό'} -
- {showIntegrationStatus && ( -
-
-

Universal autoplay system that works with any scene

- {integrationStatus && ( -
- - {integrationStatus.has_autoplay ? 'βœ… Scene supports autoplay' : '❌ No autoplay in scene'} - - {integrationStatus.has_autoplay && ( -
- - -
- - - {(autoplayIntensity * 100).toFixed(2)}% -
- - {integrationStatus.autoplay_parameters && integrationStatus.autoplay_parameters.length > 0 && ( -
-
Autoplay-Influenced Parameters:
-
    - {integrationStatus.autoplay_parameters.map((param, index) => ( -
  • - {param.name} - {param.type} -
  • - ))} -
-
- )} -
- )} -
- )} -
-
- )} -
-
+ )}
)} diff --git a/Lux/lux_react/src/components/panes/HomePane.jsx b/Lux/lux_react/src/components/panes/HomePane.jsx index 5b80f738..945b638b 100644 --- a/Lux/lux_react/src/components/panes/HomePane.jsx +++ b/Lux/lux_react/src/components/panes/HomePane.jsx @@ -3,12 +3,18 @@ import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import Divider from '@mui/material/Divider'; import Stack from '@mui/material/Stack'; +import Button from '@mui/material/Button'; import Masonry from 'react-masonry-css'; import WidgetGroup from '../WidgetGroup'; +import { BiImageAlt } from 'react-icons/bi'; +import { usePane } from './PaneContext'; +import ThumbnailCanvas from '../ThumbnailCanvas'; function HomePane({ dimensions, panelSize, panelJSON, activeGroups, onWidgetGroupChange }) { + const { setActivePane } = usePane(); const containerRef = useRef(null); const [containerWidth, setContainerWidth] = useState(null); + const [thumbnailStatus, setThumbnailStatus] = useState('loading'); // Set up ResizeObserver to track container width useEffect(() => { @@ -52,19 +58,72 @@ function HomePane({ dimensions, panelSize, panelJSON, activeGroups, onWidgetGrou return breakpoints; }, [containerWidth, panelSize]); // Only recalculate when width changes - // Filter out any group that has image picker widgets for the Home pane - const nonImageGroups = activeGroups.filter(group => { - // Skip source/target image groups entirely - if (group.name.toLowerCase().includes('source') || - group.name.toLowerCase().includes('target') || - group.name === 'SOURCE_IMAGE_GROUP' || - group.name === 'TARGET_IMAGE_GROUP' - ) - { - return false; + // Find groups that have image picker widgets + const imageGroups = activeGroups.filter(group => { + if (group.widgets) { + return group.widgets.some(widgetName => { + try { + const widgetJSON = window.module?.get_widget_JSON(widgetName); + const widget = JSON.parse(widgetJSON); + return widget?.tool === 'image'; + } catch (error) { + return false; + } + }); } + return false; + }); - // For other groups, check if they have image pickers + // Get the active/selected image from any image group + const getActiveImage = () => { + for (const group of imageGroups) { + if (group.widgets) { + for (const widgetName of group.widgets) { + try { + const widgetJSON = window.module?.get_widget_JSON(widgetName); + const widget = JSON.parse(widgetJSON); + if (widget?.tool === 'image' && widget?.items && Array.isArray(widget.items)) { + // Check for selected index in the same way as ImagePane/MasonryImagePicker + let selectedIdx = -1; + if (widget.choice !== undefined && Number.isInteger(widget.choice)) { + selectedIdx = widget.choice; + } else if (widget.selected !== undefined && Number.isInteger(widget.selected)) { + selectedIdx = widget.selected; + } else if (widget.value !== undefined && Number.isInteger(widget.value)) { + selectedIdx = widget.value; + } + + const activeItem = selectedIdx >= 0 && selectedIdx < widget.items.length + ? widget.items[selectedIdx] + : (widget.items.length > 0 ? widget.items[0] : null); + + if (activeItem) { + return { path: activeItem, name: widget.name || widgetName }; + } + } + } catch (error) { + console.error(`Error parsing widget JSON for ${widgetName}:`, error); + } + } + } + } + return null; + }; + + const activeImage = getActiveImage(); + + // Determine if the active image is source or target based on widget name + const getImageLabel = () => { + if (!activeImage) return "Images"; + const widgetName = activeImage.name.toLowerCase(); + if (widgetName.includes('source')) return "Source Image"; + if (widgetName.includes('target')) return "Target Image"; + return "Images"; + }; + + // Filter out any group that has image picker widgets for the Home pane + const nonImageGroups = activeGroups.filter(group => { + // For groups, check if they have image pickers if (group.widgets) { const hasImageWidget = group.widgets.some(widgetName => { try { @@ -205,6 +264,61 @@ function HomePane({ dimensions, panelSize, panelJSON, activeGroups, onWidgetGrou width: '100%' }} > + {imageGroups.length > 0 && ( + + + + )} + {nonImageGroups.length > 0 ? ( diff --git a/Lux/lux_react/src/components/panes/SourceImagePane.jsx b/Lux/lux_react/src/components/panes/ImagePane.jsx similarity index 58% rename from Lux/lux_react/src/components/panes/SourceImagePane.jsx rename to Lux/lux_react/src/components/panes/ImagePane.jsx index 58a17381..c7f70868 100644 --- a/Lux/lux_react/src/components/panes/SourceImagePane.jsx +++ b/Lux/lux_react/src/components/panes/ImagePane.jsx @@ -3,13 +3,17 @@ import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import Divider from '@mui/material/Divider'; import Alert from '@mui/material/Alert'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import Select from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; import { useTheme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; import WidgetGroup from '../WidgetGroup'; import MasonryImagePicker from '../MasonryImagePicker'; import {usePane} from "./PaneContext.jsx"; -function SourceImagePane({ dimensions, panelSize, panelJSON, activeGroups, onWidgetGroupChange }) { +function ImagePane({ dimensions, panelSize, panelJSON, activeGroups, onWidgetGroupChange }) { const { setActivePane } = usePane(); const [debugInfo, setDebugInfo] = useState({ groups: [], @@ -64,17 +68,55 @@ function SourceImagePane({ dimensions, panelSize, panelJSON, activeGroups, onWid // Log panel size and measured width for debugging useEffect(() => { - console.log(`SourceImagePane: Panel size ${panelSize}px, Container width ${containerWidth}px, Controls width ${controlsWidth}px, Wide layout: ${isWideLayout}`); + console.log(`ImagePane: Panel size ${panelSize}px, Container width ${containerWidth}px, Controls width ${controlsWidth}px, Wide layout: ${isWideLayout}`); }, [panelSize, containerWidth, controlsWidth, isWideLayout]); - // Look for any source-related group name - const sourceImageGroup = activeGroups.find(group => - group.name === 'SOURCE_IMAGE_GROUP' || - group.name === 'source' || - group.name === 'source_image_group' || - group.name.toLowerCase().includes('source') + // Find all groups that contain widgets with "image" tool + const imageGroups = activeGroups.filter(group => { + if (!group.widgets) return false; + + return group.widgets.some(widgetName => { + try { + const widgetJSON = window.module?.get_widget_JSON(widgetName); + const widget = JSON.parse(widgetJSON); + return widget?.tool === 'image'; + } catch (error) { + return false; + } + }); + }); + + // Get all image picker widgets from all image groups + const allImagePickers = imageGroups.flatMap(group => + group.widgets?.filter(widgetName => { + try { + const widgetJSON = window.module?.get_widget_JSON(widgetName); + const widget = JSON.parse(widgetJSON); + return widget?.tool === 'image'; + } catch (error) { + return false; + } + }).map(widgetName => ({ + widgetName, + groupName: group.name, + displayName: widgetName.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) + })) || [] ); + // State for selected image picker + const [selectedPickerId, setSelectedPickerId] = useState(allImagePickers[0]?.widgetName || ''); + + // Update selected picker when image pickers change + useEffect(() => { + if (allImagePickers.length > 0 && !allImagePickers.find(p => p.widgetName === selectedPickerId)) { + setSelectedPickerId(allImagePickers[0].widgetName); + } + }, [allImagePickers, selectedPickerId]); + + // Find the currently selected image group and picker + const selectedPicker = allImagePickers.find(p => p.widgetName === selectedPickerId); + const imageGroup = selectedPicker ? imageGroups.find(group => group.name === selectedPicker.groupName) : imageGroups[0]; + // Collect debug information @@ -113,27 +155,25 @@ function SourceImagePane({ dimensions, panelSize, panelJSON, activeGroups, onWid setDebugInfo({ groups: groupInfo, imageWidgets: imageWidgetsInfo, - selectedGroup: sourceImageGroup ? sourceImageGroup.name : null + selectedGroup: imageGroup ? imageGroup.name : null }); } - }, [activeGroups, sourceImageGroup]); + }, [activeGroups, imageGroup]); - // Find the image picker widget JSON if it exists - const imagePickerJson = sourceImageGroup?.widgets - ?.map(widgetName => { - const widgetJSON = window.module?.get_widget_JSON(widgetName); - try { - const widget = JSON.parse(widgetJSON); - return widget; - } catch (error) { - console.error(`Error parsing widget JSON for ${widgetName}:`, error); - return null; - } - }) - .find(widget => widget?.tool === 'image'); + // Find the selected image picker widget JSON + const imagePickerJson = selectedPickerId ? (() => { + try { + const widgetJSON = window.module?.get_widget_JSON(selectedPickerId); + const widget = JSON.parse(widgetJSON); + return widget?.tool === 'image' ? widget : null; + } catch (error) { + console.error(`Error parsing widget JSON for ${selectedPickerId}:`, error); + return null; + } + })() : null; // Filter out the image picker widget to avoid showing it twice - const nonImagePickerWidgets = sourceImageGroup?.widgets?.filter(widgetName => { + const nonImagePickerWidgets = imageGroup?.widgets?.filter(widgetName => { const widgetJSON = window.module?.get_widget_JSON(widgetName); try { const widget = JSON.parse(widgetJSON); @@ -144,8 +184,8 @@ function SourceImagePane({ dimensions, panelSize, panelJSON, activeGroups, onWid } }) || []; - const customSourceImageGroup = sourceImageGroup ? { - ...sourceImageGroup, + const customImageGroup = imageGroup ? { + ...imageGroup, widgets: nonImagePickerWidgets } : null; @@ -158,10 +198,10 @@ function SourceImagePane({ dimensions, panelSize, panelJSON, activeGroups, onWid // Give more space to whichever side has more widgets const getLayoutRatio = () => { // Default to 50/50 split - if (!customSourceImageGroup || !imagePickerJson) return 0.5; + if (!customImageGroup || !imagePickerJson) return 0.5; // Count widgets to determine space distribution - const widgetCount = customSourceImageGroup.widgets.length; + const widgetCount = customImageGroup.widgets.length; const imageCount = imagePickerJson.items?.length || 0; // Adjust ratio based on content (min 0.35, max 0.65) @@ -195,18 +235,45 @@ function SourceImagePane({ dimensions, panelSize, panelJSON, activeGroups, onWid width: shouldUseSideBySide ? `${imageRatio * 100}%` : '100%', }} > + {allImagePickers && allImagePickers.length >= 1 && ( + + + Image Picker + + + + )} + )} {/* Effect Controls Section */} - {customSourceImageGroup && customSourceImageGroup.widgets.length > 0 && ( + {customImageGroup && customImageGroup.widgets.length > 0 && ( )} {/* Show errors and empty states */} - {sourceImageGroup && !imagePickerJson && ( + {imageGroup && !imagePickerJson && ( - Source image group found, but no image picker widget detected. + Image group found, but no image picker widget detected. )} - {!sourceImageGroup && ( + {imageGroups.length === 0 && ( - No source image controls available. Please check your scene configuration. + No image controls available. Please check your scene configuration. )} @@ -245,4 +312,4 @@ function SourceImagePane({ dimensions, panelSize, panelJSON, activeGroups, onWid ); } -export default SourceImagePane; \ No newline at end of file +export default ImagePane; diff --git a/Lux/lux_react/src/components/panes/TargetImagePane.jsx b/Lux/lux_react/src/components/panes/TargetImagePane.jsx deleted file mode 100644 index ca37a532..00000000 --- a/Lux/lux_react/src/components/panes/TargetImagePane.jsx +++ /dev/null @@ -1,169 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import Divider from '@mui/material/Divider'; -import Alert from '@mui/material/Alert'; -import { useTheme } from '@mui/material/styles'; -import useMediaQuery from '@mui/material/useMediaQuery'; -import WidgetGroup from '../WidgetGroup'; -import MasonryImagePicker from '../MasonryImagePicker'; - -function TargetImagePane({ dimensions, panelSize, panelJSON, activeGroups, onWidgetGroupChange }) { - const [debugInfo, setDebugInfo] = useState({ - groups: [], - imageWidgets: [], - selectedGroup: null - }); - - const theme = useTheme(); - const isWideLayout = useMediaQuery(theme.breakpoints.up('md')); - - // Look for any target-related group name - const targetImageGroup = activeGroups.find(group => - group.name === 'TARGET_IMAGE_GROUP' || - group.name === 'target' || - group.name.toLowerCase().includes('target') - ); - - // Collect debug information on initial render - useEffect(() => { - if (window.module) { - // Extract useful debug info - const groupInfo = activeGroups.map(group => ({ - name: group.name, - widgetCount: group.widgets ? group.widgets.length : 0 - })); - - // Find widgets that might be image pickers - const imageWidgetsInfo = []; - activeGroups.forEach(group => { - if (group.widgets) { - group.widgets.forEach(widgetName => { - try { - const widgetJSON = window.module.get_widget_JSON(widgetName); - const widget = JSON.parse(widgetJSON); - if (widget && (widget.tool === 'image' || widget.type === 'menu_string')) { - imageWidgetsInfo.push({ - name: widgetName, - group: group.name, - type: widget.type, - tool: widget.tool, - items: widget.items ? widget.items.length : 0 - }); - } - } catch (error) { - console.error(`Error parsing widget JSON for ${widgetName}:`, error); - } - }); - } - }); - - setDebugInfo({ - groups: groupInfo, - imageWidgets: imageWidgetsInfo, - selectedGroup: targetImageGroup ? targetImageGroup.name : null - }); - } - }, [activeGroups, targetImageGroup]); - - // Find the image picker widget JSON if it exists - const imagePickerJson = targetImageGroup?.widgets - ?.map(widgetName => { - const widgetJSON = window.module?.get_widget_JSON(widgetName); - try { - const widget = JSON.parse(widgetJSON); - return widget; - } catch (error) { - console.error(`Error parsing widget JSON for ${widgetName}:`, error); - return null; - } - }) - .find(widget => widget?.tool === 'image'); - - // Filter out the image picker widget to avoid showing it twice - const nonImagePickerWidgets = targetImageGroup?.widgets?.filter(widgetName => { - const widgetJSON = window.module?.get_widget_JSON(widgetName); - try { - const widget = JSON.parse(widgetJSON); - return !(widget?.tool === 'image' || - (widget?.type === 'menu_string' && widget?.items && Array.isArray(widget.items))); - } catch (error) { - return true; - } - }) || []; - - const customTargetImageGroup = targetImageGroup ? { - ...targetImageGroup, - widgets: nonImagePickerWidgets - } : null; - - return ( - - {/* Image Picker Section */} - - {/* Show error if no image picker is found but target image group exists */} - {targetImageGroup && !imagePickerJson && ( - - Target image group found, but no image picker widget detected. - - )} - - {/* MasonryImagePicker component */} - {imagePickerJson && ( - - )} - - - {/* Effect Controls Section */} - {customTargetImageGroup && customTargetImageGroup.widgets.length > 0 && ( - - - Effect Controls - - - - - )} - - {/* Show when no target image group is found */} - {!targetImageGroup && ( - - - No target image controls available. Please check your scene configuration. - - - )} - - ); -} - -export default TargetImagePane; \ No newline at end of file