diff --git a/crates/microflow-core/src/codegen/transformation/range_map.rs b/crates/microflow-core/src/codegen/transformation/range_map.rs index 0974aa4..6af687c 100644 --- a/crates/microflow-core/src/codegen/transformation/range_map.rs +++ b/crates/microflow-core/src/codegen/transformation/range_map.rs @@ -57,9 +57,16 @@ pub fn emit(node: &FlowNode, inputs: &NodeInputs) -> NodeEmission { } if let Some(source) = sources.first() { let input = source.value.as_double_parsing(); - let mapped = format!( - "(({input} - {in_min}) * ({out_max} - {out_min}) / ({in_max} - {in_min}) + {out_min})" - ); + // A zero-width `from` range (min == max) divides by zero in C++ + // (inf/nan). The range is fixed at codegen time, so collapse it to the + // low output bound instead of emitting the division. + let mapped = if (config.from.max - config.from.min).abs() < f64::EPSILON { + out_min.clone() + } else { + format!( + "(({input} - {in_min}) * ({out_max} - {out_min}) / ({in_max} - {in_min}) + {out_min})" + ) + }; e.loop_body .push(format!("{var} = round(({mapped}) * {factor}) / {factor};")); } @@ -107,6 +114,20 @@ mod tests { assert!(body.contains("round(")); } + #[test] + fn zero_width_from_range_collapses_to_output_min_without_dividing() { + let e = emit( + &rm("r-1", json!({ "from": { "min": 5.0, "max": 5.0 }, "to": { "min": 0.0, "max": 255.0 } })), + &value_input(CppExpr::number("sensor_s_1_value")), + ); + let body = e.loop_body.join("\n"); + // A zero-width `from` range maps to the low output bound instead of + // dividing by the zero span (which would emit inf/nan C++). + assert!(body.contains("0.0")); + assert!(!body.contains("sensor_s_1_value")); + assert!(!body.contains("(5.0 - 5.0)")); + } + #[test] fn small_output_span_uses_decimal_precision() { let e = emit( diff --git a/crates/microflow-core/src/runtime/transformation/range_map.rs b/crates/microflow-core/src/runtime/transformation/range_map.rs index 05a9785..49b09f9 100644 --- a/crates/microflow-core/src/runtime/transformation/range_map.rs +++ b/crates/microflow-core/src/runtime/transformation/range_map.rs @@ -40,7 +40,14 @@ impl RangeMap { let out_min = self.config.to.min; let out_max = self.config.to.max; - let mapped = ((input_num - in_min) * (out_max - out_min)) / (in_max - in_min) + out_min; + // A zero-width `from` range (min == max) would divide by zero and + // produce inf/nan, which then propagates to whatever this Node drives. + // Collapse the degenerate range to the low output bound instead. + let mapped = if (in_max - in_min).abs() < f64::EPSILON { + out_min + } else { + ((input_num - in_min) * (out_max - out_min)) / (in_max - in_min) + out_min + }; let distance = (out_max - out_min).abs(); let precision = i32::from(distance <= 10.0); let factor = 10_f64.powi(precision);