1- import { useState , useEffect } from 'react'
1+ import { useState , useEffect , useRef } from 'react'
22import { useTranslation } from 'react-i18next'
3+ import { useSearchParams } from 'react-router-dom'
34import { Play , Settings , AlertCircle , CheckCircle , XCircle , Loader2 , Download , Scale , BookOpen } from 'lucide-react'
45import { useDatabase } from '../contexts/DatabaseContext'
56import { useQueryState } from '../contexts/QueryStateContext'
@@ -12,53 +13,143 @@ import MaterialsCatalog from '../components/MaterialsCatalog'
1213import DatabaseLoadingCard from '../components/DatabaseLoadingCard'
1314import { getAllElements } from '../services/queryService'
1415import { createEqualProportions } from '../services/proportionService'
16+ import { getMaterialById } from '../constants/materials'
1517import type { CascadeResults , Element , WeightedNuclide , ProportionFormat } from '../types'
1618
19+ // Parse URL search params into cascade state (returns null fields when param is absent)
20+ function parseUrlParams ( searchParams : URLSearchParams ) {
21+ const has = searchParams . toString ( ) . length > 0
22+ if ( ! has ) return null
23+
24+ const str = ( key : string ) => searchParams . get ( key )
25+ const num = ( key : string ) => { const v = searchParams . get ( key ) ; if ( v === null ) return undefined ; const n = parseFloat ( v ) ; return isNaN ( n ) ? undefined : n }
26+ const bool = ( key : string ) => { const v = searchParams . get ( key ) ; return v !== null ? v === 'true' : undefined }
27+ const list = ( key : string ) => { const v = searchParams . get ( key ) ; return v ? v . split ( ',' ) : undefined }
28+
29+ // Resolve ?material= to fuel nuclides + weighted mode
30+ const materialId = str ( 'material' )
31+ let materialFuel : WeightedNuclide [ ] | undefined
32+ let materialNuclides : string [ ] | undefined
33+ if ( materialId ) {
34+ const mat = getMaterialById ( materialId )
35+ if ( mat ) {
36+ materialFuel = mat . composition . map ( c => ( {
37+ nuclideId : c . nuclideId ,
38+ proportion : c . proportion ,
39+ sourceType : 'material' as const ,
40+ } ) )
41+ materialNuclides = mat . composition . map ( c => c . nuclideId )
42+ }
43+ }
44+
45+ return {
46+ fuel : list ( 'fuel' ) ?? materialNuclides ,
47+ temperature : num ( 'temp' ) ,
48+ minFusionMeV : num ( 'minFusionMeV' ) ,
49+ minTwoToTwoMeV : num ( 'minTwoToTwoMeV' ) ,
50+ maxLoops : num ( 'maxLoops' ) ,
51+ maxNuclides : num ( 'maxNuclides' ) ,
52+ feedbackBosons : bool ( 'feedbackBosons' ) ,
53+ feedbackFermions : bool ( 'feedbackFermions' ) ,
54+ allowDimers : bool ( 'allowDimers' ) ,
55+ excludeMelted : bool ( 'excludeMelted' ) ,
56+ excludeBoiledOff : bool ( 'excludeBoiledOff' ) ,
57+ materialFuel,
58+ useWeightedMode : materialFuel ? true : undefined ,
59+ }
60+ }
61+
62+ const DEFAULT_PARAMS = {
63+ temperature : 2400 ,
64+ minFusionMeV : 1.0 ,
65+ minTwoToTwoMeV : 1.0 ,
66+ maxNuclides : 5000 ,
67+ maxLoops : 25 ,
68+ feedbackBosons : true ,
69+ feedbackFermions : true ,
70+ allowDimers : true ,
71+ excludeMelted : false ,
72+ excludeBoiledOff : true ,
73+ }
74+
75+ const DEFAULT_FUEL = [ 'H-1' , 'Li-7' , 'Al-27' , 'N-14' , 'Ni-58' , 'Ni-60' , 'Ni-62' , 'B-10' , 'B-11' ]
76+
1777export default function CascadesAll ( ) {
1878 const { t } = useTranslation ( )
1979 const { db, isLoading : dbLoading , error : dbError , downloadProgress } = useDatabase ( )
2080 const { getCascadeState, updateCascadeState } = useQueryState ( )
2181 const { runCascade, cancelCascade, progress, isRunning, error : workerError } = useCascadeWorker ( )
82+ const [ searchParams , setSearchParams ] = useSearchParams ( )
83+ const hasUrlParams = useRef ( searchParams . toString ( ) . length > 0 )
84+ const isInitialMount = useRef ( true )
2285 const [ hasRestoredFromContext , setHasRestoredFromContext ] = useState ( false )
2386
24- const [ params , setParams ] = useState ( {
25- temperature : 2400 ,
26- minFusionMeV : 1.0 ,
27- minTwoToTwoMeV : 1.0 ,
28- maxNuclides : 5000 ,
29- maxLoops : 25 ,
30- feedbackBosons : true ,
31- feedbackFermions : true ,
32- allowDimers : true ,
33- excludeMelted : false ,
34- excludeBoiledOff : true ,
87+ // Parse URL params once on mount
88+ const urlState = useRef ( parseUrlParams ( searchParams ) )
89+
90+ const [ params , setParams ] = useState ( ( ) => {
91+ const u = urlState . current
92+ if ( ! u ) return { ...DEFAULT_PARAMS }
93+ return {
94+ temperature : u . temperature ?? DEFAULT_PARAMS . temperature ,
95+ minFusionMeV : u . minFusionMeV ?? DEFAULT_PARAMS . minFusionMeV ,
96+ minTwoToTwoMeV : u . minTwoToTwoMeV ?? DEFAULT_PARAMS . minTwoToTwoMeV ,
97+ maxNuclides : u . maxNuclides ?? DEFAULT_PARAMS . maxNuclides ,
98+ maxLoops : u . maxLoops ?? DEFAULT_PARAMS . maxLoops ,
99+ feedbackBosons : u . feedbackBosons ?? DEFAULT_PARAMS . feedbackBosons ,
100+ feedbackFermions : u . feedbackFermions ?? DEFAULT_PARAMS . feedbackFermions ,
101+ allowDimers : u . allowDimers ?? DEFAULT_PARAMS . allowDimers ,
102+ excludeMelted : u . excludeMelted ?? DEFAULT_PARAMS . excludeMelted ,
103+ excludeBoiledOff : u . excludeBoiledOff ?? DEFAULT_PARAMS . excludeBoiledOff ,
104+ }
35105 } )
36106
37107 // Local state for sliders during dragging (prevents performance issues)
38- const [ sliderMaxNuclides , setSliderMaxNuclides ] = useState ( 5000 )
39- const [ sliderMaxLoops , setSliderMaxLoops ] = useState ( 25 )
108+ const [ sliderMaxNuclides , setSliderMaxNuclides ] = useState ( ( ) => urlState . current ?. maxNuclides ?? DEFAULT_PARAMS . maxNuclides )
109+ const [ sliderMaxLoops , setSliderMaxLoops ] = useState ( ( ) => urlState . current ?. maxLoops ?? DEFAULT_PARAMS . maxLoops )
40110
41111 const [ availableElements , setAvailableElements ] = useState < Element [ ] > ( [ ] )
42- const [ fuelNuclides , setFuelNuclides ] = useState < string [ ] > ( [ 'H-1' , 'Li-7' , 'Al-27' , 'N-14' , 'Ni-58' , 'Ni-60' , 'Ni-62' , 'B-10' , 'B-11' ] )
112+ const [ fuelNuclides , setFuelNuclides ] = useState < string [ ] > ( ( ) => urlState . current ?. fuel ?? DEFAULT_FUEL )
43113 const [ results , setResults ] = useState < CascadeResults | null > ( null )
44114 const [ error , setError ] = useState < string | null > ( null )
45115
46116 // Weighted mode state (Issue #96)
47- const [ useWeightedMode , setUseWeightedMode ] = useState ( false )
48- const [ weightedFuel , setWeightedFuel ] = useState < WeightedNuclide [ ] > ( [ ] )
117+ const [ useWeightedMode , setUseWeightedMode ] = useState ( ( ) => urlState . current ?. useWeightedMode ?? false )
118+ const [ weightedFuel , setWeightedFuel ] = useState < WeightedNuclide [ ] > ( ( ) => urlState . current ?. materialFuel ?? [ ] )
49119 const [ proportionFormat , setProportionFormat ] = useState < ProportionFormat > ( 'percentage' )
50120 const [ showMaterialsCatalog , setShowMaterialsCatalog ] = useState ( false )
51121
122+ // Sync state → URL params (skip initial mount)
123+ useEffect ( ( ) => {
124+ if ( isInitialMount . current ) {
125+ isInitialMount . current = false
126+ return
127+ }
128+ const p = new URLSearchParams ( )
129+ if ( fuelNuclides . join ( ',' ) !== DEFAULT_FUEL . join ( ',' ) ) p . set ( 'fuel' , fuelNuclides . join ( ',' ) )
130+ if ( params . temperature !== DEFAULT_PARAMS . temperature ) p . set ( 'temp' , String ( params . temperature ) )
131+ if ( params . minFusionMeV !== DEFAULT_PARAMS . minFusionMeV ) p . set ( 'minFusionMeV' , String ( params . minFusionMeV ) )
132+ if ( params . minTwoToTwoMeV !== DEFAULT_PARAMS . minTwoToTwoMeV ) p . set ( 'minTwoToTwoMeV' , String ( params . minTwoToTwoMeV ) )
133+ if ( params . maxLoops !== DEFAULT_PARAMS . maxLoops ) p . set ( 'maxLoops' , String ( params . maxLoops ) )
134+ if ( params . maxNuclides !== DEFAULT_PARAMS . maxNuclides ) p . set ( 'maxNuclides' , String ( params . maxNuclides ) )
135+ if ( params . feedbackBosons !== DEFAULT_PARAMS . feedbackBosons ) p . set ( 'feedbackBosons' , String ( params . feedbackBosons ) )
136+ if ( params . feedbackFermions !== DEFAULT_PARAMS . feedbackFermions ) p . set ( 'feedbackFermions' , String ( params . feedbackFermions ) )
137+ if ( params . allowDimers !== DEFAULT_PARAMS . allowDimers ) p . set ( 'allowDimers' , String ( params . allowDimers ) )
138+ if ( params . excludeMelted !== DEFAULT_PARAMS . excludeMelted ) p . set ( 'excludeMelted' , String ( params . excludeMelted ) )
139+ if ( params . excludeBoiledOff !== DEFAULT_PARAMS . excludeBoiledOff ) p . set ( 'excludeBoiledOff' , String ( params . excludeBoiledOff ) )
140+ setSearchParams ( p , { replace : true } )
141+ } , [ fuelNuclides , params , setSearchParams ] )
142+
52143 // Load available elements and restore state when database is ready
53144 useEffect ( ( ) => {
54145 if ( db ) {
55146 const elements = getAllElements ( db )
56147 setAvailableElements ( elements )
57148
58- // Restore state from context if not already done
149+ // Restore state from context if not already done (skip if URL params provided)
59150 if ( ! hasRestoredFromContext ) {
60151 const savedState = getCascadeState ( )
61- if ( savedState ) {
152+ if ( savedState && ! hasUrlParams . current ) {
62153 setParams ( {
63154 temperature : savedState . temperature ,
64155 minFusionMeV : savedState . minFusionMeV ,
@@ -216,21 +307,10 @@ export default function CascadesAll() {
216307 }
217308
218309 const handleReset = ( ) => {
219- setParams ( {
220- temperature : 2400 ,
221- minFusionMeV : 1.0 ,
222- minTwoToTwoMeV : 1.0 ,
223- maxNuclides : 5000 ,
224- maxLoops : 25 ,
225- feedbackBosons : true ,
226- feedbackFermions : true ,
227- allowDimers : true ,
228- excludeMelted : false ,
229- excludeBoiledOff : true ,
230- } )
231- setSliderMaxNuclides ( 5000 )
232- setSliderMaxLoops ( 25 )
233- setFuelNuclides ( [ 'H-1' , 'Li-7' , 'Al-27' , 'N-14' , 'Ni-58' , 'Ni-60' , 'Ni-62' , 'B-10' , 'B-11' ] )
310+ setParams ( { ...DEFAULT_PARAMS } )
311+ setSliderMaxNuclides ( DEFAULT_PARAMS . maxNuclides )
312+ setSliderMaxLoops ( DEFAULT_PARAMS . maxLoops )
313+ setFuelNuclides ( [ ...DEFAULT_FUEL ] )
234314 setResults ( null )
235315 setError ( null )
236316 // Reset weighted mode state
0 commit comments