Skip to main content

brep_kernel/solvers/sketch_solver/
jsnum.rs

1use super::*;
2
3// ---------------------------------------------------------------------------
4// JavaScript numeric semantics helpers
5// ---------------------------------------------------------------------------
6
7/// JavaScript `Math.round` (ties round toward +Infinity).
8pub(super) fn js_round(x: f64) -> f64 {
9    if !x.is_finite() {
10        return x;
11    }
12    let floor = x.floor();
13    let diff = x - floor;
14    if diff >= 0.5 {
15        floor + 1.0
16    } else {
17        floor
18    }
19}
20
21/// mathHelpersMod.roundToDecimals.
22pub(super) fn round_to_decimals(number: f64, decimals: i32) -> f64 {
23    let k = 10f64.powi(decimals);
24    js_round(number * k) / k
25}
26
27/// JavaScript `Math.sign` (preserves signed zero, NaN passes through).
28pub(super) fn js_sign(x: f64) -> f64 {
29    if x.is_nan() || x == 0.0 {
30        x
31    } else if x > 0.0 {
32        1.0
33    } else {
34        -1.0
35    }
36}
37
38/// JavaScript `parseFloat` applied to a JSON value (numbers pass through, strings are
39/// parsed from their leading numeric prefix, everything else is NaN).
40pub(super) fn js_parse_float(value: Option<&Value>) -> f64 {
41    match value {
42        Some(Value::Number(number)) => number.as_f64().unwrap_or(f64::NAN),
43        Some(Value::String(text)) => parse_float_prefix(text),
44        _ => f64::NAN,
45    }
46}
47
48pub(super) fn parse_float_prefix(text: &str) -> f64 {
49    let trimmed = text.trim_start();
50    let bytes = trimmed.as_bytes();
51    let mut end = 0usize;
52    let mut seen_digit = false;
53    let mut seen_dot = false;
54    let mut seen_exp = false;
55    let mut index = 0usize;
56    if index < bytes.len() && (bytes[index] == b'+' || bytes[index] == b'-') {
57        index += 1;
58    }
59    if trimmed[index..].starts_with("Infinity") {
60        let sign = if bytes.first() == Some(&b'-') {
61            -1.0
62        } else {
63            1.0
64        };
65        return sign * f64::INFINITY;
66    }
67    while index < bytes.len() {
68        let b = bytes[index];
69        if b.is_ascii_digit() {
70            seen_digit = true;
71            end = index + 1;
72        } else if b == b'.' && !seen_dot && !seen_exp {
73            seen_dot = true;
74        } else if (b == b'e' || b == b'E') && seen_digit && !seen_exp {
75            let mut next = index + 1;
76            if next < bytes.len() && (bytes[next] == b'+' || bytes[next] == b'-') {
77                next += 1;
78            }
79            if next < bytes.len() && bytes[next].is_ascii_digit() {
80                seen_exp = true;
81                index = next;
82                end = index + 1;
83                continue;
84            } else {
85                break;
86            }
87        } else {
88            break;
89        }
90        index += 1;
91    }
92    if !seen_digit {
93        return f64::NAN;
94    }
95    trimmed[..end].parse::<f64>().unwrap_or(f64::NAN)
96}
97
98/// JavaScript `Number(...)` coercion for the value shapes that appear in sketches.
99pub(super) fn js_number(value: Option<&Value>) -> f64 {
100    match value {
101        None => f64::NAN,
102        Some(Value::Null) => 0.0,
103        Some(Value::Bool(flag)) => {
104            if *flag {
105                1.0
106            } else {
107                0.0
108            }
109        }
110        Some(Value::Number(number)) => number.as_f64().unwrap_or(f64::NAN),
111        Some(Value::String(text)) => {
112            let trimmed = text.trim();
113            if trimmed.is_empty() {
114                0.0
115            } else if trimmed == "Infinity" || trimmed == "+Infinity" {
116                f64::INFINITY
117            } else if trimmed == "-Infinity" {
118                f64::NEG_INFINITY
119            } else {
120                trimmed.parse::<f64>().unwrap_or(f64::NAN)
121            }
122        }
123        Some(_) => f64::NAN,
124    }
125}
126
127/// Coerce a raw constraint `value` the way the distance path saw it:
128/// null/undefined behave like the "seed from current" branch (NaN here),
129/// everything else uses Number coercion.
130pub(super) fn cv_from_raw(value: Option<&Value>) -> f64 {
131    match value {
132        None | Some(Value::Null) => f64::NAN,
133        other => js_number(other),
134    }
135}
136
137/// JavaScript `parseInt` for canonical constraint-point ids.
138pub(super) fn js_parse_int(value: Option<&Value>) -> Option<i64> {
139    match value {
140        Some(Value::Number(number)) => {
141            let float = number.as_f64()?;
142            if float.is_finite() {
143                Some(float.trunc() as i64)
144            } else {
145                None
146            }
147        }
148        Some(Value::String(text)) => {
149            let trimmed = text.trim_start();
150            let bytes = trimmed.as_bytes();
151            let mut index = 0usize;
152            let mut sign = 1i64;
153            if index < bytes.len() && (bytes[index] == b'+' || bytes[index] == b'-') {
154                if bytes[index] == b'-' {
155                    sign = -1;
156                }
157                index += 1;
158            }
159            let start = index;
160            while index < bytes.len() && bytes[index].is_ascii_digit() {
161                index += 1;
162            }
163            if index == start {
164                return None;
165            }
166            trimmed[start..index].parse::<i64>().ok().map(|v| sign * v)
167        }
168        _ => None,
169    }
170}
171
172/// JavaScript truthiness for JSON values.
173pub(super) fn truthy(value: Option<&Value>) -> bool {
174    match value {
175        None | Some(Value::Null) => false,
176        Some(Value::Bool(flag)) => *flag,
177        Some(Value::Number(number)) => {
178            let float = number.as_f64().unwrap_or(f64::NAN);
179            float != 0.0 && !float.is_nan()
180        }
181        Some(Value::String(text)) => !text.is_empty(),
182        Some(_) => true,
183    }
184}
185
186/// Number formatting for signatures / error strings (JavaScript-like: integral values
187/// print without a decimal point, negative zero prints as "0").
188pub(super) fn fmt_number(x: f64) -> String {
189    if x.is_nan() {
190        return "NaN".to_string();
191    }
192    if x == 0.0 {
193        return "0".to_string();
194    }
195    format!("{}", x)
196}
197
198/// Canonical sketch entity key. Numeric IDs normalize integral floats and signed
199/// zero; string IDs are preserved. Other JSON values use their JSON spelling.
200pub fn fmt_id(value: &Value) -> String {
201    match value {
202        Value::String(text) => text.clone(),
203        Value::Number(number) => number
204            .as_f64()
205            .map(fmt_number)
206            .unwrap_or_else(|| number.to_string()),
207        Value::Bool(flag) => flag.to_string(),
208        Value::Null => "null".to_string(),
209        other => other.to_string(),
210    }
211}
212
213/// JSON-safe number (JSON.stringify turns NaN/Infinity into null).
214pub(super) fn json_num(x: f64) -> Value {
215    serde_json::Number::from_f64(x)
216        .map(Value::Number)
217        .unwrap_or(Value::Null)
218}
219
220pub(super) fn normalize_angle(angle: f64) -> f64 {
221    ((angle % 360.0) + 360.0) % 360.0
222}
223
224pub(super) fn shortest_angle_delta(target: f64, current: f64) -> f64 {
225    let delta = normalize_angle(target - current);
226    if delta > 180.0 {
227        delta - 360.0
228    } else {
229        delta
230    }
231}
232
233pub(super) fn relative_delta_ratio(a: f64, b: f64, floor: f64) -> f64 {
234    let denom = a.abs().max(b.abs()).max(floor);
235    (a - b).abs() / denom
236}
237
238/// Normalized bit pattern for signature comparison: collapses -0 into +0 and
239/// all NaNs into one canonical NaN so bit equality matches JavaScript string equality.
240pub(super) fn sig_bits(x: f64) -> u64 {
241    if x == 0.0 {
242        0u64
243    } else if x.is_nan() {
244        f64::NAN.to_bits()
245    } else {
246        x.to_bits()
247    }
248}