1use std::ops::{Add, Sub};
10
11#[derive(Clone, Debug, PartialEq)]
15pub enum UnitType {
16 Npc,
18 Cm,
20 Inches,
22 Mm,
24 Points,
26 Lines,
28 Char,
30 Native,
32 Null,
34 Snpc,
36 StrWidth(String),
38 StrHeight(String),
40 GrobWidth(String),
42 GrobHeight(String),
44}
45
46impl UnitType {
47 pub fn is_absolute(&self) -> bool {
49 matches!(
50 self,
51 UnitType::Cm | UnitType::Inches | UnitType::Mm | UnitType::Points
52 )
53 }
54}
55
56#[derive(Clone, Debug)]
66pub struct UnitContext {
67 pub viewport_width_cm: f64,
69 pub viewport_height_cm: f64,
71 pub xscale: (f64, f64),
73 pub yscale: (f64, f64),
75 pub fontsize_pt: f64,
77 pub lineheight: f64,
79}
80
81impl Default for UnitContext {
82 fn default() -> Self {
83 UnitContext {
84 viewport_width_cm: 17.78, viewport_height_cm: 17.78, xscale: (0.0, 1.0),
87 yscale: (0.0, 1.0),
88 fontsize_pt: 12.0,
89 lineheight: 1.2,
90 }
91 }
92}
93
94impl UnitContext {
95 pub fn resolve_x(&self, unit: &Unit, i: usize) -> f64 {
97 let idx = i % unit.values.len();
98 resolve_one(unit.values[idx], &unit.units[idx], self, Axis::X)
99 }
100
101 pub fn resolve_y(&self, unit: &Unit, i: usize) -> f64 {
103 let idx = i % unit.values.len();
104 resolve_one(unit.values[idx], &unit.units[idx], self, Axis::Y)
105 }
106
107 pub fn resolve_size(&self, unit: &Unit, i: usize) -> f64 {
109 let idx = i % unit.values.len();
110 let x = resolve_one(unit.values[idx], &unit.units[idx], self, Axis::X);
111 let y = resolve_one(unit.values[idx], &unit.units[idx], self, Axis::Y);
112 (x.abs() * y.abs()).sqrt()
113 }
114}
115
116#[derive(Clone, Copy, Debug, PartialEq, Eq)]
123pub enum Axis {
124 X,
125 Y,
126}
127
128#[derive(Clone, Debug)]
138pub struct Unit {
139 pub values: Vec<f64>,
141 pub units: Vec<UnitType>,
143}
144
145impl Unit {
146 pub fn new(value: f64, unit_type: UnitType) -> Self {
148 Unit {
149 values: vec![value],
150 units: vec![unit_type],
151 }
152 }
153
154 pub fn npc(value: f64) -> Self {
156 Unit::new(value, UnitType::Npc)
157 }
158
159 pub fn cm(value: f64) -> Self {
161 Unit::new(value, UnitType::Cm)
162 }
163
164 pub fn inches(value: f64) -> Self {
166 Unit::new(value, UnitType::Inches)
167 }
168
169 pub fn mm(value: f64) -> Self {
171 Unit::new(value, UnitType::Mm)
172 }
173
174 pub fn points(value: f64) -> Self {
176 Unit::new(value, UnitType::Points)
177 }
178
179 pub fn lines(value: f64) -> Self {
181 Unit::new(value, UnitType::Lines)
182 }
183
184 pub fn null(value: f64) -> Self {
186 Unit::new(value, UnitType::Null)
187 }
188
189 pub fn value(&self) -> f64 {
191 *self.values.first().unwrap_or(&0.0)
192 }
193
194 pub fn len(&self) -> usize {
196 self.values.len()
197 }
198
199 pub fn is_empty(&self) -> bool {
201 self.values.is_empty()
202 }
203
204 pub fn is_absolute(&self) -> bool {
206 self.units.iter().all(UnitType::is_absolute)
207 }
208
209 pub fn to_cm(&self, ctx: &UnitContext, axis: Axis) -> Vec<f64> {
215 if self.values.is_empty() {
216 return vec![];
217 }
218
219 let resolved: Vec<f64> = self
222 .values
223 .iter()
224 .zip(self.units.iter())
225 .map(|(&val, unit_type)| resolve_one(val, unit_type, ctx, axis))
226 .collect();
227
228 resolved
229 }
230
231 pub fn to_cm_scalar(&self, ctx: &UnitContext, axis: Axis) -> f64 {
236 self.values
237 .iter()
238 .zip(self.units.iter())
239 .map(|(&val, unit_type)| resolve_one(val, unit_type, ctx, axis))
240 .sum()
241 }
242
243 pub fn scale(mut self, factor: f64) -> Self {
245 for v in &mut self.values {
246 *v *= factor;
247 }
248 self
249 }
250}
251
252fn resolve_one(value: f64, unit_type: &UnitType, ctx: &UnitContext, axis: Axis) -> f64 {
254 let viewport_cm = match axis {
255 Axis::X => ctx.viewport_width_cm,
256 Axis::Y => ctx.viewport_height_cm,
257 };
258
259 match unit_type {
260 UnitType::Npc => value * viewport_cm,
261 UnitType::Cm => value,
262 UnitType::Inches => value * 2.54,
263 UnitType::Mm => value / 10.0,
264 UnitType::Points => value / 72.0 * 2.54,
265 UnitType::Lines => {
266 value * ctx.fontsize_pt * ctx.lineheight / 72.0 * 2.54
268 }
269 UnitType::Char => {
270 value * ctx.fontsize_pt * 0.6 / 72.0 * 2.54
272 }
273 UnitType::Native => {
274 let (scale_min, scale_max) = match axis {
276 Axis::X => ctx.xscale,
277 Axis::Y => ctx.yscale,
278 };
279 let range = scale_max - scale_min;
280 if range.abs() < f64::EPSILON {
281 0.0
282 } else {
283 let npc = (value - scale_min) / range;
284 npc * viewport_cm
285 }
286 }
287 UnitType::Null => {
288 0.0
290 }
291 UnitType::Snpc => {
292 let min_dim = ctx.viewport_width_cm.min(ctx.viewport_height_cm);
294 value * min_dim
295 }
296 UnitType::StrWidth(s) => {
297 let char_count = s.len() as f64;
299 char_count * ctx.fontsize_pt * 0.6 / 72.0 * 2.54
300 }
301 UnitType::StrHeight(_) => {
302 ctx.fontsize_pt / 72.0 * 2.54
304 }
305 UnitType::GrobWidth(_) => {
306 value * 3.0
310 }
311 UnitType::GrobHeight(_) => {
312 value * 1.0
314 }
315 }
316}
317
318impl Add for Unit {
319 type Output = Unit;
320
321 fn add(mut self, mut rhs: Unit) -> Unit {
322 self.values.append(&mut rhs.values);
323 self.units.append(&mut rhs.units);
324 self
325 }
326}
327
328impl Sub for Unit {
329 type Output = Unit;
330
331 fn sub(mut self, mut rhs: Unit) -> Unit {
332 for v in &mut rhs.values {
334 *v = -*v;
335 }
336 self.values.append(&mut rhs.values);
337 self.units.append(&mut rhs.units);
338 self
339 }
340}
341
342#[cfg(test)]
345mod tests {
346 use super::*;
347
348 const EPSILON: f64 = 1e-10;
349
350 fn approx_eq(a: f64, b: f64) -> bool {
351 (a - b).abs() < EPSILON
352 }
353
354 #[test]
355 fn unit_cm_passthrough() {
356 let u = Unit::cm(2.5);
357 let ctx = UnitContext::default();
358 let result = u.to_cm_scalar(&ctx, Axis::X);
359 assert!(approx_eq(result, 2.5), "expected 2.5, got {result}");
360 }
361
362 #[test]
363 fn unit_inches_conversion() {
364 let u = Unit::inches(1.0);
365 let ctx = UnitContext::default();
366 let result = u.to_cm_scalar(&ctx, Axis::X);
367 assert!(approx_eq(result, 2.54), "expected 2.54, got {result}");
368 }
369
370 #[test]
371 fn unit_mm_conversion() {
372 let u = Unit::mm(10.0);
373 let ctx = UnitContext::default();
374 let result = u.to_cm_scalar(&ctx, Axis::X);
375 assert!(approx_eq(result, 1.0), "expected 1.0, got {result}");
376 }
377
378 #[test]
379 fn unit_points_conversion() {
380 let u = Unit::points(72.0);
382 let ctx = UnitContext::default();
383 let result = u.to_cm_scalar(&ctx, Axis::X);
384 assert!(approx_eq(result, 2.54), "expected 2.54, got {result}");
385 }
386
387 #[test]
388 fn unit_npc_uses_viewport_dimension() {
389 let u = Unit::npc(0.5);
390 let ctx = UnitContext {
391 viewport_width_cm: 20.0,
392 viewport_height_cm: 10.0,
393 ..Default::default()
394 };
395 let x = u.to_cm_scalar(&ctx, Axis::X);
397 assert!(approx_eq(x, 10.0), "expected 10.0, got {x}");
398 let y = u.to_cm_scalar(&ctx, Axis::Y);
400 assert!(approx_eq(y, 5.0), "expected 5.0, got {y}");
401 }
402
403 #[test]
404 fn unit_addition_combines_values() {
405 let a = Unit::cm(1.0);
406 let b = Unit::mm(5.0);
407 let combined = a + b;
408 assert_eq!(combined.len(), 2);
409 let ctx = UnitContext::default();
410 let result = combined.to_cm_scalar(&ctx, Axis::X);
411 assert!(approx_eq(result, 1.5), "expected 1.5, got {result}");
412 }
413
414 #[test]
415 fn unit_subtraction() {
416 let a = Unit::cm(3.0);
417 let b = Unit::cm(1.0);
418 let combined = a - b;
419 let ctx = UnitContext::default();
420 let result = combined.to_cm_scalar(&ctx, Axis::X);
421 assert!(approx_eq(result, 2.0), "expected 2.0, got {result}");
422 }
423
424 #[test]
425 fn unit_scale() {
426 let u = Unit::cm(2.0).scale(3.0);
427 let ctx = UnitContext::default();
428 let result = u.to_cm_scalar(&ctx, Axis::X);
429 assert!(approx_eq(result, 6.0), "expected 6.0, got {result}");
430 }
431
432 #[test]
433 fn unit_null_resolves_to_zero() {
434 let u = Unit::null(5.0);
435 let ctx = UnitContext::default();
436 let result = u.to_cm_scalar(&ctx, Axis::X);
437 assert!(approx_eq(result, 0.0), "expected 0.0, got {result}");
438 }
439
440 #[test]
441 fn unit_native_maps_through_scale() {
442 let u = Unit::new(50.0, UnitType::Native);
443 let ctx = UnitContext {
444 viewport_width_cm: 10.0,
445 xscale: (0.0, 100.0),
446 ..Default::default()
447 };
448 let result = u.to_cm_scalar(&ctx, Axis::X);
450 assert!(approx_eq(result, 5.0), "expected 5.0, got {result}");
451 }
452
453 #[test]
454 fn unit_is_absolute() {
455 assert!(Unit::cm(1.0).is_absolute());
456 assert!(Unit::inches(1.0).is_absolute());
457 assert!(Unit::mm(1.0).is_absolute());
458 assert!(Unit::points(1.0).is_absolute());
459 assert!(!Unit::npc(0.5).is_absolute());
460 assert!(!Unit::null(1.0).is_absolute());
461 assert!(!Unit::lines(1.0).is_absolute());
462 }
463
464 #[test]
465 fn unit_snpc_uses_smaller_dimension() {
466 let u = Unit::new(1.0, UnitType::Snpc);
467 let ctx = UnitContext {
468 viewport_width_cm: 20.0,
469 viewport_height_cm: 10.0,
470 ..Default::default()
471 };
472 let result = u.to_cm_scalar(&ctx, Axis::X);
474 assert!(approx_eq(result, 10.0), "expected 10.0, got {result}");
475 }
476
477 #[test]
478 fn unit_empty() {
479 let u = Unit {
480 values: vec![],
481 units: vec![],
482 };
483 assert!(u.is_empty());
484 assert_eq!(u.to_cm(&UnitContext::default(), Axis::X).len(), 0);
485 }
486}