@@ -195,25 +195,34 @@ export default function InteractiveDemo() {
195195 const deltaRingR = delta * PH * 0.48 ;
196196 const BASE_MARGIN = 5 ; // px margin when robustness off
197197
198- function isDisabled ( action ) {
199- if ( nonActionable && ( action . feature === 'savings' || action . feature === 'both' ) ) return true ;
200- if ( sparsity && action . feature === 'both' ) return true ;
198+ function isDisabled ( action , na = nonActionable , sp = sparsity ) {
199+ if ( na && ( action . feature === 'savings' || action . feature === 'both' ) ) return true ;
200+ if ( sp && action . feature === 'both' ) return true ;
201201 return false ;
202202 }
203203
204- // Compute current endpoint + cost for the selected action
205- const { endpoint : selEndpoint , cost : selCost } = useMemo ( ( ) => {
206- const action = ACTIONS . find ( a => a . id === selectedAction ) ;
207- if ( ! action || isDisabled ( action ) ) return { endpoint : null , cost : 0 } ;
204+ // Compute live endpoints + costs for ALL actions whenever anything relevant changes.
205+ // This means every action badge shows an up-to-date cost reflecting the current
206+ // boundary shape, robustness level, and constraint set.
207+ const allEndpoints = useMemo ( ( ) => {
208208 const targetDist = robustness ? deltaRingR : BASE_MARGIN ;
209- const ep = computeEndpoint ( selectedAction , boundaryType , targetDist ) ;
210- // Scale cost proportionally to how far the endpoint moved vs. reference
211- const baseDist = Math . hypot ( action . baseTo [ 0 ] - 25 , action . baseTo [ 1 ] - 30 ) ;
212- const epDist = Math . hypot ( ep [ 0 ] - 25 , ep [ 1 ] - 30 ) ;
213- const cost = + ( action . cost * Math . max ( 1 , epDist / Math . max ( baseDist , 1 ) ) ) . toFixed ( 1 ) ;
214- return { endpoint : ep , cost } ;
209+ const result = { } ;
210+ for ( const action of ACTIONS ) {
211+ if ( isDisabled ( action ) ) continue ;
212+ const ep = computeEndpoint ( action . id , boundaryType , targetDist ) ;
213+ const baseDist = Math . hypot ( action . baseTo [ 0 ] - 25 , action . baseTo [ 1 ] - 30 ) ;
214+ const epDist = Math . hypot ( ep [ 0 ] - 25 , ep [ 1 ] - 30 ) ;
215+ result [ action . id ] = {
216+ ep,
217+ cost : + ( action . cost * Math . max ( 1 , epDist / Math . max ( baseDist , 1 ) ) ) . toFixed ( 1 ) ,
218+ } ;
219+ }
220+ return result ;
215221 // eslint-disable-next-line react-hooks/exhaustive-deps
216- } , [ selectedAction , boundaryType , robustness , deltaRingR ] ) ;
222+ } , [ boundaryType , robustness , deltaRingR , nonActionable , sparsity ] ) ;
223+
224+ const selEndpoint = allEndpoints [ selectedAction ] ?. ep ?? null ;
225+ const selCost = allEndpoints [ selectedAction ] ?. cost ?? 0 ;
217226
218227 const youPt = toSvg ( 25 , 30 ) ;
219228 const formulation = buildFormulation ( nonActionable , robustness , sparsity , delta ) ;
@@ -396,8 +405,8 @@ export default function InteractiveDemo() {
396405 { ACTIONS . map ( action => {
397406 const disabled = isDisabled ( action ) ;
398407 const active = selectedAction === action . id && ! disabled ;
399- // Show dynamic cost when this action is active, else base cost
400- const displayCost = active ? selCost : action . cost ;
408+ // All buttons show live cost — updates with model type, δ, and constraints
409+ const liveCost = allEndpoints [ action . id ] ?. cost ?? action . cost ;
401410 return (
402411 < button key = { action . id } type = "button" disabled = { disabled }
403412 className = { `demo-action${ active ? ' active' : '' } ${ disabled ? ' disabled' : '' } ` }
@@ -407,7 +416,7 @@ export default function InteractiveDemo() {
407416 < span className = "demo-action-label" > { action . label } </ span >
408417 < span className = "demo-action-cost"
409418 style = { { background : action . soft , color : action . color } } >
410- { disabled ? 'blocked' : `cost ${ displayCost } ` }
419+ { disabled ? 'blocked' : `cost ${ liveCost } ` }
411420 </ span >
412421 </ button >
413422 ) ;
@@ -421,15 +430,31 @@ export default function InteractiveDemo() {
421430 < div className = "demo-toggle-list" >
422431 < label className = "demo-toggle" >
423432 < input type = "checkbox" checked = { nonActionable }
424- onChange = { e => { setNonActionable ( e . target . checked ) ; setSelectedAction ( null ) ; } } />
433+ onChange = { e => {
434+ const checked = e . target . checked ;
435+ setNonActionable ( checked ) ;
436+ // Only deselect if the active action becomes blocked by this constraint
437+ if ( checked && selectedAction ) {
438+ const sel = ACTIONS . find ( a => a . id === selectedAction ) ;
439+ if ( sel && isDisabled ( sel , checked , sparsity ) ) setSelectedAction ( null ) ;
440+ }
441+ } } />
425442 < span className = "demo-toggle-track" />
426443 < span className = "demo-toggle-text" >
427444 Non-actionable: < em > Savings Rate</ em >
428445 </ span >
429446 </ label >
430447 < label className = "demo-toggle" >
431448 < input type = "checkbox" checked = { sparsity }
432- onChange = { e => { setSparsity ( e . target . checked ) ; setSelectedAction ( null ) ; } } />
449+ onChange = { e => {
450+ const checked = e . target . checked ;
451+ setSparsity ( checked ) ;
452+ // Only deselect if the active action becomes blocked by this constraint
453+ if ( checked && selectedAction ) {
454+ const sel = ACTIONS . find ( a => a . id === selectedAction ) ;
455+ if ( sel && isDisabled ( sel , nonActionable , checked ) ) setSelectedAction ( null ) ;
456+ }
457+ } } />
433458 < span className = "demo-toggle-track" />
434459 < span className = "demo-toggle-text" > Sparsity < em > k = 1</ em > (one feature only)</ span >
435460 </ label >
0 commit comments