55 */
66
77import { predictStrengthCurve , predictStrengthMeanOnly , predictGWP , predictCost , initStrengthModel , initWASM } from "./gp.mjs" ;
8+ import {
9+ UNITS ,
10+ compToDisplay ,
11+ compFromDisplay ,
12+ sliderUnitLabel as sliderUnitLabelFor ,
13+ } from "./units.mjs" ;
814
915// --- Shared Helpers ---
1016function easeInOutCubic ( t ) {
@@ -68,20 +74,8 @@ let _msCurveTransition = null; // {startTime, duration, times, fromMeans, fromSt
6874
6975// --- Unit System ---
7076let unitSystem = "metric" ; // "metric" or "imperial"
71- const UNITS = {
72- metric : {
73- strength : "MPa" , strengthFactor : 1 / 145.04 , // psi → MPa
74- mass : "kg/m³" , massFactor : 1 ,
75- gwp : "kg CO₂/m³" , gwpFactor : 1 ,
76- cost : "$/m³" , costFactor : 1 ,
77- } ,
78- imperial : {
79- strength : "psi" , strengthFactor : 1 , // already psi
80- mass : "lb/yd³" , massFactor : 1.6856 , // kg/m³ → lb/yd³
81- gwp : "lb CO₂/yd³" , gwpFactor : 1.6856 ,
82- cost : "$/yd³" , costFactor : 1 / 1.30795 , // $/m³ → $/yd³
83- } ,
84- } ;
77+ // `UNITS` is imported from `./units.mjs` (single source of truth, also used
78+ // by the Node-based `test/test_js_units.mjs` parity tests).
8579function U ( ) { return UNITS [ unitSystem ] ; }
8680
8781// Animated unit transition
@@ -294,11 +288,22 @@ function buildSliders() {
294288 // negative values are ever entered (no need for `-` key on iOS Safari).
295289 valueInput . inputMode = "decimal" ;
296290 valueInput . setAttribute ( "aria-label" , `${ shortName } value` ) ;
297- valueInput . value = ( currentComposition [ i ] * sliderDisplayFactor ( col ) ) . toFixed ( 1 ) ;
291+ valueInput . value = displayCompValue ( col , currentComposition [ i ] ) . toFixed ( 1 ) ;
298292 valueInput . dataset . idx = i ;
299293 valueInput . dataset . col = col ;
300294 attachValueEditHandlers ( valueInput , i , col , b ) ;
301- label . append ( nameSpan , valueInput ) ;
295+
296+ // Per-row unit suffix (kg/m³ ↔ lb/yd³ on toggle; °C for Temperature).
297+ // Wrapped in a flex container so the label keeps its two-child
298+ // `space-between` layout (name on the left, value+unit packed on the right).
299+ const valueWrap = document . createElement ( "span" ) ;
300+ valueWrap . className = "slider-value-wrap" ;
301+ const unitSpan = document . createElement ( "span" ) ;
302+ unitSpan . className = "slider-unit" ;
303+ unitSpan . id = `unit-${ i } ` ;
304+ unitSpan . textContent = sliderUnitLabel ( col ) ;
305+ valueWrap . append ( valueInput , unitSpan ) ;
306+ label . append ( nameSpan , valueWrap ) ;
302307
303308 const input = document . createElement ( "input" ) ;
304309 input . type = "range" ;
@@ -378,7 +383,7 @@ function syncSliderDOM(comp, updateValues = true) {
378383 for ( const slider of _sliderInputs ) {
379384 const idx = parseInt ( slider . dataset . idx ) ;
380385 if ( updateValues ) slider . value = comp [ idx ] ;
381- setValueDisplay ( idx , ( comp [ idx ] * sliderDisplayFactor ( slider . dataset . col ) ) . toFixed ( 1 ) ) ;
386+ setValueDisplay ( idx , displayCompValue ( slider . dataset . col , comp [ idx ] ) . toFixed ( 1 ) ) ;
382387 }
383388}
384389
@@ -447,7 +452,7 @@ function onSliderChange(e) {
447452 const idx = parseInt ( e . target . dataset . idx ) ;
448453 currentComposition [ idx ] = parseFloat ( e . target . value ) ;
449454 displayPreviewComp [ idx ] = currentComposition [ idx ] ;
450- const displayVal = currentComposition [ idx ] * sliderDisplayFactor ( e . target . dataset . col ) ;
455+ const displayVal = displayCompValue ( e . target . dataset . col , currentComposition [ idx ] ) ;
451456 setValueDisplay ( idx , displayVal . toFixed ( 1 ) ) ;
452457 _sliderActive = true ;
453458 if ( _sliderIdleTimer ) clearTimeout ( _sliderIdleTimer ) ;
@@ -458,14 +463,23 @@ function onSliderChange(e) {
458463 checkExtrapolationWarning ( ) ;
459464}
460465
461- // Display factor for a slider column — temperature is never converted
462- function sliderDisplayFactor ( colName ) {
463- if ( colName . includes ( "Temp" ) ) return 1 ;
464- return U ( ) . massFactor ;
466+ // Display value for a composition column under the active unit system.
467+ // Handles both mass (factor) and temperature (factor + offset).
468+ function displayCompValue ( colName , internal ) {
469+ return compToDisplay ( colName , internal , unitSystem ) ;
470+ }
471+ // Inverse of `displayCompValue`: parse a user-typed display value back to
472+ // the model-native (kg/m³ or °C) value before clamping/storage.
473+ function internalCompValue ( colName , display ) {
474+ return compFromDisplay ( colName , display , unitSystem ) ;
475+ }
476+ // Unit suffix label for a slider column (delegates to `units.mjs`).
477+ function sliderUnitLabel ( colName ) {
478+ return sliderUnitLabelFor ( colName , unitSystem ) ;
465479}
466480function updateSliderLabels ( ) {
467481 syncSliderDOM ( currentComposition , false ) ;
468- // Update info rows (min/max labels)
482+ // Update info rows (min/max labels) and per-row unit suffixes
469483 const bounds = compositionsData . slider_bounds ;
470484 const colNames = compositionsData . column_names ;
471485 const infoRows = document . querySelectorAll ( "#sliders .info-row" ) ;
@@ -476,12 +490,16 @@ function updateSliderLabels() {
476490 const b = bounds [ col ] ;
477491 if ( b . min === b . max ) continue ;
478492 if ( rowIdx < infoRows . length ) {
479- const factor = sliderDisplayFactor ( col ) ;
480- const minDisp = ( b . min * factor ) . toFixed ( 0 ) ;
481- const maxDisp = ( b . max * factor ) . toFixed ( 0 ) ;
493+ // Use offset-aware converter so temperature bounds render correctly
494+ // in °F (e.g. -20°C → -4°F, 22°C → 72°F) under imperial.
495+ const minDisp = displayCompValue ( col , b . min ) . toFixed ( 0 ) ;
496+ const maxDisp = displayCompValue ( col , b . max ) . toFixed ( 0 ) ;
482497 infoRows [ rowIdx ] . innerHTML = `<span>${ minDisp } </span><span>${ maxDisp } </span>` ;
483498 rowIdx ++ ;
484499 }
500+ // Refresh per-row unit suffix (kg/m³ ↔ lb/yd³, °C ↔ °F)
501+ const unitEl = document . getElementById ( `unit-${ i } ` ) ;
502+ if ( unitEl ) unitEl . textContent = sliderUnitLabel ( col ) ;
485503 }
486504}
487505
@@ -585,11 +603,12 @@ function attachValueEditHandlers(inputEl, idx, col, b) {
585603 const parsed = parseFloat ( raw ) ;
586604 if ( ! Number . isFinite ( parsed ) ) {
587605 // Non-numeric → revert displayed text
588- inputEl . value = ( currentComposition [ idx ] * sliderDisplayFactor ( col ) ) . toFixed ( 1 ) ;
606+ inputEl . value = displayCompValue ( col , currentComposition [ idx ] ) . toFixed ( 1 ) ;
589607 return ;
590608 }
591- // Convert displayed value back to internal units, then clamp
592- const internal = parsed / sliderDisplayFactor ( col ) ;
609+ // Convert displayed value back to internal units, then clamp.
610+ // For Temperature this also handles the °F → °C offset.
611+ const internal = internalCompValue ( col , parsed ) ;
593612 const clamped = Math . max ( b . min , Math . min ( b . max , internal ) ) ;
594613 // Build target from the most recent intended end state to avoid landing
595614 // mid-animation values for sliders that are currently in flight.
@@ -602,7 +621,7 @@ function attachValueEditHandlers(inputEl, idx, col, b) {
602621 // will overwrite, but we want the input to read correctly during the lerp
603622 // since `setValueDisplay` skips focused elements — and this element is
604623 // still focused if commit was triggered by Enter).
605- inputEl . value = ( clamped * sliderDisplayFactor ( col ) ) . toFixed ( 1 ) ;
624+ inputEl . value = displayCompValue ( col , clamped ) . toFixed ( 1 ) ;
606625 }
607626 inputEl . addEventListener ( "focus" , ( ) => inputEl . select ( ) ) ;
608627 inputEl . addEventListener ( "keydown" , ( e ) => {
@@ -613,14 +632,14 @@ function attachValueEditHandlers(inputEl, idx, col, b) {
613632 } else if ( e . key === "Escape" ) {
614633 e . preventDefault ( ) ;
615634 // Revert without committing
616- inputEl . value = ( currentComposition [ idx ] * sliderDisplayFactor ( col ) ) . toFixed ( 1 ) ;
635+ inputEl . value = displayCompValue ( col , currentComposition [ idx ] ) . toFixed ( 1 ) ;
617636 inputEl . blur ( ) ;
618637 }
619638 } ) ;
620639 inputEl . addEventListener ( "blur" , ( ) => {
621640 // Blur commits the edit (same as Enter), but only if the value was changed.
622641 // If the value matches the current displayed state, do nothing.
623- const expected = ( currentComposition [ idx ] * sliderDisplayFactor ( col ) ) . toFixed ( 1 ) ;
642+ const expected = displayCompValue ( col , currentComposition [ idx ] ) . toFixed ( 1 ) ;
624643 if ( inputEl . value . trim ( ) !== expected ) commit ( ) ;
625644 } ) ;
626645}
0 commit comments