1use crate::core::{
16 area, constants, cross_product_two_vectors, dot_product_two_vectors, ellipse_point64,
17 get_segment_intersect_pt_d, reflect_point, strip_duplicates_path, translate_point, Path64,
18 PathD, Paths64, Point64, PointD, Rect64,
19};
20use crate::engine::ClipType;
21use crate::engine_public::{Clipper64, PolyTree64};
22use crate::FillRule;
23
24const FLOATING_POINT_TOLERANCE: f64 = 1e-12;
29
30const ARC_CONST: f64 = 0.002;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48pub enum JoinType {
49 Square,
50 Bevel,
51 Round,
52 Miter,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64pub enum EndType {
65 Polygon,
66 Joined,
67 Butt,
68 Square,
69 Round,
70}
71
72pub type DeltaCallback64 = Box<dyn Fn(&Path64, &PathD, usize, usize) -> f64>;
76
77fn get_lowest_closed_path_info(paths: &Paths64) -> (Option<usize>, bool) {
84 let mut idx: Option<usize> = None;
85 let mut bot_pt = Point64::new(i64::MAX, i64::MIN);
86 let mut is_neg_area = false;
87
88 for (i, path_i) in paths.iter().enumerate() {
89 let mut a: f64 = f64::MAX;
90 for pt in path_i {
91 if (pt.y < bot_pt.y) || (pt.y == bot_pt.y && pt.x >= bot_pt.x) {
92 continue;
93 }
94 if a == f64::MAX {
95 a = area(path_i);
96 if a == 0.0 {
97 break; }
99 is_neg_area = a < 0.0;
100 }
101 idx = Some(i);
102 bot_pt.x = pt.x;
103 bot_pt.y = pt.y;
104 }
105 }
106 (idx, is_neg_area)
107}
108
109#[inline]
112fn hypot_xy(x: f64, y: f64) -> f64 {
113 (x * x + y * y).sqrt()
114}
115
116#[inline]
119fn get_unit_normal(pt1: &Point64, pt2: &Point64) -> PointD {
120 if pt1 == pt2 {
121 return PointD::new(0.0, 0.0);
122 }
123 let dx = (pt2.x - pt1.x) as f64;
124 let dy = (pt2.y - pt1.y) as f64;
125 let inverse_hypot = 1.0 / hypot_xy(dx, dy);
126 PointD::new(dy * inverse_hypot, -dx * inverse_hypot)
127}
128
129#[inline]
132fn almost_zero(value: f64, epsilon: f64) -> bool {
133 value.abs() < epsilon
134}
135
136#[inline]
139fn normalize_vector(vec: &PointD) -> PointD {
140 let h = hypot_xy(vec.x, vec.y);
141 if almost_zero(h, 0.001) {
142 return PointD::new(0.0, 0.0);
143 }
144 let inverse_hypot = 1.0 / h;
145 PointD::new(vec.x * inverse_hypot, vec.y * inverse_hypot)
146}
147
148#[inline]
151fn get_avg_unit_vector(vec1: &PointD, vec2: &PointD) -> PointD {
152 normalize_vector(&PointD::new(vec1.x + vec2.x, vec1.y + vec2.y))
153}
154
155#[inline]
158#[allow(dead_code)]
159fn is_closed_path(et: EndType) -> bool {
160 et == EndType::Polygon || et == EndType::Joined
161}
162
163#[inline]
166fn get_perpendic(pt: &Point64, norm: &PointD, delta: f64) -> Point64 {
167 Point64::new(
168 (pt.x as f64 + norm.x * delta).round() as i64,
169 (pt.y as f64 + norm.y * delta).round() as i64,
170 )
171}
172
173#[inline]
176fn get_perpendic_d(pt: &Point64, norm: &PointD, delta: f64) -> PointD {
177 PointD::new(pt.x as f64 + norm.x * delta, pt.y as f64 + norm.y * delta)
178}
179
180#[inline]
183fn negate_path(path: &mut PathD) {
184 for pt in path.iter_mut() {
185 pt.x = -pt.x;
186 pt.y = -pt.y;
187 }
188}
189
190#[inline]
193fn point64_from_f(x: f64, y: f64) -> Point64 {
194 Point64::new(x.round() as i64, y.round() as i64)
195}
196
197struct Group {
204 paths_in: Paths64,
205 lowest_path_idx: Option<usize>,
206 is_reversed: bool,
207 join_type: JoinType,
208 end_type: EndType,
209}
210
211impl Group {
212 fn new(paths: &Paths64, join_type: JoinType, end_type: EndType) -> Self {
215 let mut paths_in = paths.clone();
216 let is_joined = end_type == EndType::Polygon || end_type == EndType::Joined;
217
218 for p in paths_in.iter_mut() {
219 strip_duplicates_path(p, is_joined);
220 }
221
222 let (lowest_path_idx, is_reversed) = if end_type == EndType::Polygon {
223 let (idx, is_neg_area) = get_lowest_closed_path_info(&paths_in);
224 let is_reversed = idx.is_some() && is_neg_area;
228 (idx, is_reversed)
229 } else {
230 (None, false)
231 };
232
233 Group {
234 paths_in,
235 lowest_path_idx,
236 is_reversed,
237 join_type,
238 end_type,
239 }
240 }
241}
242
243pub struct ClipperOffset {
254 error_code: i32,
256 delta: f64,
257 group_delta: f64,
258 temp_lim: f64,
259 steps_per_rad: f64,
260 step_sin: f64,
261 step_cos: f64,
262 norms: PathD,
263 path_out: Path64,
264 solution: Paths64,
265 groups: Vec<Group>,
266 join_type: JoinType,
267 end_type: EndType,
268
269 miter_limit: f64,
271 arc_tolerance: f64,
272 preserve_collinear: bool,
273 reverse_solution: bool,
274
275 delta_callback: Option<DeltaCallback64>,
277}
278
279impl ClipperOffset {
280 pub fn new(
287 miter_limit: f64,
288 arc_tolerance: f64,
289 preserve_collinear: bool,
290 reverse_solution: bool,
291 ) -> Self {
292 ClipperOffset {
293 error_code: 0,
294 delta: 0.0,
295 group_delta: 0.0,
296 temp_lim: 0.0,
297 steps_per_rad: 0.0,
298 step_sin: 0.0,
299 step_cos: 0.0,
300 norms: PathD::new(),
301 path_out: Path64::new(),
302 solution: Paths64::new(),
303 groups: Vec::new(),
304 join_type: JoinType::Bevel,
305 end_type: EndType::Polygon,
306 miter_limit,
307 arc_tolerance,
308 preserve_collinear,
309 reverse_solution,
310 delta_callback: None,
311 }
312 }
313
314 pub fn new_default() -> Self {
318 Self::new(2.0, 0.0, false, false)
319 }
320
321 pub fn error_code(&self) -> i32 {
323 self.error_code
324 }
325
326 pub fn miter_limit(&self) -> f64 {
328 self.miter_limit
329 }
330
331 pub fn set_miter_limit(&mut self, miter_limit: f64) {
333 self.miter_limit = miter_limit;
334 }
335
336 pub fn arc_tolerance(&self) -> f64 {
338 self.arc_tolerance
339 }
340
341 pub fn set_arc_tolerance(&mut self, arc_tolerance: f64) {
344 self.arc_tolerance = arc_tolerance;
345 }
346
347 pub fn preserve_collinear(&self) -> bool {
349 self.preserve_collinear
350 }
351
352 pub fn set_preserve_collinear(&mut self, preserve_collinear: bool) {
354 self.preserve_collinear = preserve_collinear;
355 }
356
357 pub fn reverse_solution(&self) -> bool {
359 self.reverse_solution
360 }
361
362 pub fn set_reverse_solution(&mut self, reverse_solution: bool) {
364 self.reverse_solution = reverse_solution;
365 }
366
367 pub fn set_delta_callback(&mut self, cb: Option<DeltaCallback64>) {
370 self.delta_callback = cb;
371 }
372
373 pub fn add_path(&mut self, path: &Path64, jt: JoinType, et: EndType) {
380 self.groups.push(Group::new(&vec![path.clone()], jt, et));
381 }
382
383 pub fn add_paths(&mut self, paths: &Paths64, jt: JoinType, et: EndType) {
386 if paths.is_empty() {
387 return;
388 }
389 self.groups.push(Group::new(paths, jt, et));
390 }
391
392 pub fn clear(&mut self) {
395 self.groups.clear();
396 self.norms.clear();
397 }
398
399 pub fn execute(&mut self, delta: f64, paths: &mut Paths64) {
407 paths.clear();
408 self.solution.clear();
409 self.execute_internal(delta, None);
410 std::mem::swap(&mut self.solution, paths);
411 }
412
413 pub fn execute_tree(&mut self, delta: f64, polytree: &mut PolyTree64) {
417 polytree.clear();
418 self.solution.clear();
419 self.execute_internal(delta, Some(polytree));
420 self.solution.clear();
421 }
422
423 pub fn execute_with_callback(&mut self, delta_cb: DeltaCallback64, paths: &mut Paths64) {
427 self.delta_callback = Some(delta_cb);
428 self.execute(1.0, paths);
429 }
430
431 fn calc_solution_capacity(&self) -> usize {
438 let mut result = 0;
439 for g in &self.groups {
440 result += if g.end_type == EndType::Joined {
441 g.paths_in.len() * 2
442 } else {
443 g.paths_in.len()
444 };
445 }
446 result
447 }
448
449 fn check_reverse_orientation(&self) -> bool {
452 for g in &self.groups {
454 if g.end_type == EndType::Polygon {
455 return g.is_reversed;
456 }
457 }
458 false
459 }
460
461 fn execute_internal(&mut self, delta: f64, polytree: Option<&mut PolyTree64>) {
464 self.error_code = 0;
465 if self.groups.is_empty() {
466 return;
467 }
468 self.solution.reserve(self.calc_solution_capacity());
469
470 if delta.abs() < 0.5 {
471 let mut sol_size = 0;
473 for group in &self.groups {
474 sol_size += group.paths_in.len();
475 }
476 self.solution.reserve(sol_size);
477 for group in &self.groups {
478 self.solution.extend(group.paths_in.iter().cloned());
479 }
480 } else {
481 self.temp_lim = if self.miter_limit <= 1.0 {
482 2.0
483 } else {
484 2.0 / (self.miter_limit * self.miter_limit)
485 };
486
487 self.delta = delta;
488 for i in 0..self.groups.len() {
491 self.do_group_offset(i);
492 if self.error_code != 0 {
493 self.solution.clear();
494 }
495 }
496 }
497
498 if self.solution.is_empty() {
499 return;
500 }
501
502 let paths_reversed = self.check_reverse_orientation();
503 let mut c = Clipper64::new();
505 c.set_preserve_collinear(self.preserve_collinear);
506 c.set_reverse_solution(self.reverse_solution != paths_reversed);
508 c.add_subject(&self.solution);
509
510 let fill_rule = if paths_reversed {
511 FillRule::Negative
512 } else {
513 FillRule::Positive
514 };
515
516 if let Some(tree) = polytree {
517 let mut open_paths = Paths64::new();
518 c.execute_tree(ClipType::Union, fill_rule, tree, &mut open_paths);
519 } else {
520 c.execute(ClipType::Union, fill_rule, &mut self.solution, None);
521 }
522 }
523
524 fn build_normals(&mut self, path: &Path64) {
531 self.norms.clear();
532 if path.is_empty() {
533 return;
534 }
535 self.norms.reserve(path.len());
536 for i in 0..path.len() - 1 {
537 self.norms.push(get_unit_normal(&path[i], &path[i + 1]));
538 }
539 self.norms
540 .push(get_unit_normal(path.last().unwrap(), &path[0]));
541 }
542
543 fn do_bevel(&mut self, path: &Path64, j: usize, k: usize) {
550 let pt1: PointD;
551 let pt2: PointD;
552 if j == k {
553 let abs_delta = self.group_delta.abs();
554 pt1 = PointD::new(
555 path[j].x as f64 - abs_delta * self.norms[j].x,
556 path[j].y as f64 - abs_delta * self.norms[j].y,
557 );
558 pt2 = PointD::new(
559 path[j].x as f64 + abs_delta * self.norms[j].x,
560 path[j].y as f64 + abs_delta * self.norms[j].y,
561 );
562 } else {
563 pt1 = PointD::new(
564 path[j].x as f64 + self.group_delta * self.norms[k].x,
565 path[j].y as f64 + self.group_delta * self.norms[k].y,
566 );
567 pt2 = PointD::new(
568 path[j].x as f64 + self.group_delta * self.norms[j].x,
569 path[j].y as f64 + self.group_delta * self.norms[j].y,
570 );
571 }
572 self.path_out.push(point64_from_f(pt1.x, pt1.y));
573 self.path_out.push(point64_from_f(pt2.x, pt2.y));
574 }
575
576 fn do_square(&mut self, path: &Path64, j: usize, k: usize) {
579 let vec: PointD = if j == k {
580 PointD::new(self.norms[j].y, -self.norms[j].x)
581 } else {
582 get_avg_unit_vector(
583 &PointD::new(-self.norms[k].y, self.norms[k].x),
584 &PointD::new(self.norms[j].y, -self.norms[j].x),
585 )
586 };
587
588 let abs_delta = self.group_delta.abs();
589
590 let pt_q = PointD::new(path[j].x as f64, path[j].y as f64);
592 let pt_q = translate_point(&pt_q, abs_delta * vec.x, abs_delta * vec.y);
593 let pt1 = translate_point(&pt_q, self.group_delta * vec.y, self.group_delta * -vec.x);
595 let pt2 = translate_point(&pt_q, self.group_delta * -vec.y, self.group_delta * vec.x);
596 let pt3 = get_perpendic_d(&path[k], &self.norms[k], self.group_delta);
598
599 if j == k {
600 let pt4 = PointD::new(
601 pt3.x + vec.x * self.group_delta,
602 pt3.y + vec.y * self.group_delta,
603 );
604 let mut pt = pt_q;
605 get_segment_intersect_pt_d(pt1, pt2, pt3, pt4, &mut pt);
606 let reflected = reflect_point(&pt, &pt_q);
608 self.path_out.push(point64_from_f(reflected.x, reflected.y));
609 self.path_out.push(point64_from_f(pt.x, pt.y));
610 } else {
611 let pt4 = get_perpendic_d(&path[j], &self.norms[k], self.group_delta);
612 let mut pt = pt_q;
613 get_segment_intersect_pt_d(pt1, pt2, pt3, pt4, &mut pt);
614 self.path_out.push(point64_from_f(pt.x, pt.y));
615 let reflected = reflect_point(&pt, &pt_q);
617 self.path_out.push(point64_from_f(reflected.x, reflected.y));
618 }
619 }
620
621 fn do_miter(&mut self, path: &Path64, j: usize, k: usize, cos_a: f64) {
624 let q = self.group_delta / (cos_a + 1.0);
625 self.path_out.push(point64_from_f(
626 path[j].x as f64 + (self.norms[k].x + self.norms[j].x) * q,
627 path[j].y as f64 + (self.norms[k].y + self.norms[j].y) * q,
628 ));
629 }
630
631 fn do_round(&mut self, path: &Path64, j: usize, k: usize, angle: f64) {
634 if self.delta_callback.is_some() {
635 let abs_delta = self.group_delta.abs();
638 let arc_tol = if self.arc_tolerance > FLOATING_POINT_TOLERANCE {
639 abs_delta.min(self.arc_tolerance)
640 } else {
641 abs_delta * ARC_CONST
642 };
643 let steps_per_360 =
644 (constants::PI / (1.0 - arc_tol / abs_delta).acos()).min(abs_delta * constants::PI);
645 self.step_sin = (2.0 * constants::PI / steps_per_360).sin();
646 self.step_cos = (2.0 * constants::PI / steps_per_360).cos();
647 if self.group_delta < 0.0 {
648 self.step_sin = -self.step_sin;
649 }
650 self.steps_per_rad = steps_per_360 / (2.0 * constants::PI);
651 }
652
653 let pt = path[j];
654 let mut offset_vec = PointD::new(
655 self.norms[k].x * self.group_delta,
656 self.norms[k].y * self.group_delta,
657 );
658
659 if j == k {
660 offset_vec = offset_vec.negate();
661 }
662 self.path_out.push(point64_from_f(
663 pt.x as f64 + offset_vec.x,
664 pt.y as f64 + offset_vec.y,
665 ));
666
667 let steps = (self.steps_per_rad * angle.abs()).ceil() as i32; for _ in 1..steps {
669 offset_vec = PointD::new(
671 offset_vec.x * self.step_cos - self.step_sin * offset_vec.y,
672 offset_vec.x * self.step_sin + offset_vec.y * self.step_cos,
673 );
674 self.path_out.push(point64_from_f(
675 pt.x as f64 + offset_vec.x,
676 pt.y as f64 + offset_vec.y,
677 ));
678 }
679 self.path_out
680 .push(get_perpendic(&path[j], &self.norms[j], self.group_delta));
681 }
682
683 fn offset_point(&mut self, group_idx: usize, path: &Path64, j: usize, k: usize) {
690 if path[j] == path[k] {
697 return;
698 }
699
700 let sin_a = cross_product_two_vectors(self.norms[j], self.norms[k]);
701 let cos_a = dot_product_two_vectors(self.norms[j], self.norms[k]);
702 let sin_a = sin_a.clamp(-1.0, 1.0);
703
704 if let Some(ref cb) = self.delta_callback {
705 self.group_delta = cb(path, &self.norms, j, k);
706 if self.groups[group_idx].is_reversed {
707 self.group_delta = -self.group_delta;
708 }
709 }
710 if self.group_delta.abs() <= FLOATING_POINT_TOLERANCE {
711 self.path_out.push(path[j]);
712 return;
713 }
714
715 if cos_a > -0.999 && (sin_a * self.group_delta < 0.0) {
716 self.path_out
724 .push(get_perpendic(&path[j], &self.norms[k], self.group_delta));
725 self.path_out.push(path[j]); self.path_out
727 .push(get_perpendic(&path[j], &self.norms[j], self.group_delta));
728 } else if cos_a > 0.999 && self.join_type != JoinType::Round {
729 self.do_miter(path, j, k, cos_a);
731 } else if self.join_type == JoinType::Miter {
732 if cos_a > self.temp_lim - 1.0 {
734 self.do_miter(path, j, k, cos_a);
735 } else {
736 self.do_square(path, j, k);
737 }
738 } else if self.join_type == JoinType::Round {
739 self.do_round(path, j, k, sin_a.atan2(cos_a));
740 } else if self.join_type == JoinType::Bevel {
741 self.do_bevel(path, j, k);
742 } else {
743 self.do_square(path, j, k);
744 }
745 }
746
747 fn offset_polygon(&mut self, group_idx: usize, path: &Path64) {
750 self.path_out.clear();
751 let len = path.len();
752 if len == 0 {
753 return;
754 }
755 let mut k = len - 1;
756 for j in 0..len {
757 self.offset_point(group_idx, path, j, k);
758 k = j;
759 }
760 let path_out = std::mem::take(&mut self.path_out);
761 self.solution.push(path_out);
762 }
763
764 fn offset_open_joined(&mut self, group_idx: usize, path: &Path64) {
767 self.offset_polygon(group_idx, path);
768 let mut reverse_path = path.clone();
769 reverse_path.reverse();
770
771 self.norms.reverse();
773 self.norms.push(self.norms[0]);
774 self.norms.remove(0);
775 negate_path(&mut self.norms);
776
777 self.offset_polygon(group_idx, &reverse_path);
778 }
779
780 fn offset_open_path(&mut self, group_idx: usize, path: &Path64) {
783 self.path_out.clear();
784
785 if let Some(ref cb) = self.delta_callback {
787 self.group_delta = cb(path, &self.norms, 0, 0);
788 }
789
790 if self.group_delta.abs() <= FLOATING_POINT_TOLERANCE {
791 self.path_out.push(path[0]);
792 } else {
793 match self.end_type {
794 EndType::Butt => self.do_bevel(path, 0, 0),
795 EndType::Round => self.do_round(path, 0, 0, constants::PI),
796 _ => self.do_square(path, 0, 0),
797 }
798 }
799
800 let high_i = path.len() - 1;
801 let mut k = 0;
803 for j in 1..high_i {
804 self.offset_point(group_idx, path, j, k);
805 k = j;
806 }
807
808 for i in (1..=high_i).rev() {
810 self.norms[i] = PointD::new(-self.norms[i - 1].x, -self.norms[i - 1].y);
811 }
812 self.norms[0] = self.norms[high_i];
813
814 if let Some(ref cb) = self.delta_callback {
816 self.group_delta = cb(path, &self.norms, high_i, high_i);
817 }
818
819 if self.group_delta.abs() <= FLOATING_POINT_TOLERANCE {
820 self.path_out.push(path[high_i]);
821 } else {
822 match self.end_type {
823 EndType::Butt => self.do_bevel(path, high_i, high_i),
824 EndType::Round => self.do_round(path, high_i, high_i, constants::PI),
825 _ => self.do_square(path, high_i, high_i),
826 }
827 }
828
829 let mut k = high_i;
831 for j in (1..high_i).rev() {
832 self.offset_point(group_idx, path, j, k);
833 k = j;
834 }
835 let path_out = std::mem::take(&mut self.path_out);
836 self.solution.push(path_out);
837 }
838
839 fn do_group_offset(&mut self, group_idx: usize) {
846 let group_end_type = self.groups[group_idx].end_type;
847 let group_join_type = self.groups[group_idx].join_type;
848 let group_is_reversed = self.groups[group_idx].is_reversed;
849 let group_lowest = self.groups[group_idx].lowest_path_idx;
850
851 if group_end_type == EndType::Polygon {
852 if group_lowest.is_none() {
855 self.delta = self.delta.abs();
856 }
857 self.group_delta = if group_is_reversed {
858 -self.delta
859 } else {
860 self.delta
861 };
862 } else {
863 self.group_delta = self.delta.abs();
864 }
865
866 let abs_delta = self.group_delta.abs();
867 self.join_type = group_join_type;
868 self.end_type = group_end_type;
869
870 if group_join_type == JoinType::Round || group_end_type == EndType::Round {
871 let arc_tol = if self.arc_tolerance > FLOATING_POINT_TOLERANCE {
873 abs_delta.min(self.arc_tolerance)
874 } else {
875 abs_delta * ARC_CONST
876 };
877
878 let steps_per_360 =
879 (constants::PI / (1.0 - arc_tol / abs_delta).acos()).min(abs_delta * constants::PI);
880 self.step_sin = (2.0 * constants::PI / steps_per_360).sin();
881 self.step_cos = (2.0 * constants::PI / steps_per_360).cos();
882 if self.group_delta < 0.0 {
883 self.step_sin = -self.step_sin;
884 }
885 self.steps_per_rad = steps_per_360 / (2.0 * constants::PI);
886 }
887
888 let paths_count = self.groups[group_idx].paths_in.len();
890 for path_idx in 0..paths_count {
891 let path = self.groups[group_idx].paths_in[path_idx].clone();
892 let path_len = path.len();
893 self.path_out.clear();
894
895 if path_len == 1 {
896 if self.delta_callback.is_some() {
898 let cb_result = if let Some(ref cb) = self.delta_callback {
899 cb(&path, &self.norms, 0, 0)
900 } else {
901 0.0
902 };
903 self.group_delta = cb_result;
904 if group_is_reversed {
905 self.group_delta = -self.group_delta;
906 }
907 }
908
909 if self.group_delta < 1.0 {
910 continue;
911 }
912 let pt = path[0];
913 let abs_delta_local = self.group_delta.abs();
914
915 if group_join_type == JoinType::Round {
917 let radius = abs_delta_local;
918 let steps = if self.steps_per_rad > 0.0 {
919 (self.steps_per_rad * 2.0 * constants::PI).ceil() as usize
920 } else {
921 0
922 };
923 self.path_out = ellipse_point64(pt, radius, radius, steps);
924 } else {
925 let d = abs_delta_local.ceil() as i64;
926 let r = Rect64::new(pt.x - d, pt.y - d, pt.x + d, pt.y + d);
927 self.path_out = r.as_path();
928 }
929
930 let path_out = std::mem::take(&mut self.path_out);
931 self.solution.push(path_out);
932 continue;
933 } if path_len == 2 && group_end_type == EndType::Joined {
936 self.end_type = if group_join_type == JoinType::Round {
937 EndType::Round
938 } else {
939 EndType::Square
940 };
941 }
942
943 self.build_normals(&path);
944 if self.end_type == EndType::Polygon {
945 self.offset_polygon(group_idx, &path);
946 } else if self.end_type == EndType::Joined {
947 self.offset_open_joined(group_idx, &path);
948 } else {
949 self.offset_open_path(group_idx, &path);
950 }
951 }
952 }
953}
954
955#[cfg(test)]
960#[path = "offset_tests.rs"]
961mod tests;