Skip to main content

steel_utils/types/
position.rs

1use std::{
2    collections::VecDeque,
3    hash::{Hash, Hasher},
4    io::{self, Cursor, Write},
5};
6
7use glam::{DVec3, IVec2, IVec3};
8use rustc_hash::FxHashSet;
9
10use crate::{
11    axis::Axis,
12    direction::Direction,
13    serial::{ReadFrom, WriteTo},
14};
15
16use super::{
17    identifier::Identifier,
18    packed_position::{PackedBlockPos, PackedChunkPos, PackedSectionBlockPos, PackedSectionPos},
19};
20
21/// A chunk position.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct ChunkPos(pub IVec2);
24
25impl Hash for ChunkPos {
26    fn hash<H: Hasher>(&self, state: &mut H) {
27        state.write_u64(PackedChunkPos::from(*self).as_raw() as u64);
28    }
29}
30
31impl ChunkPos {
32    const OFFSETS: [(i32, i32); 8] = [
33        (-1, -1),
34        (0, -1),
35        (1, -1),
36        (-1, 0),
37        (1, 0),
38        (-1, 1),
39        (0, 1),
40        (1, 1),
41    ];
42
43    /// Safety margin in chunks for world generation dependencies.
44    /// Calculated as `(32 + GENERATION_PYRAMID.getStepTo(FULL).accumulatedDependencies().size() + 1) * 2`.
45    /// The accumulated dependencies size for FULL is 9 (radius 8 + 1).
46    const SAFETY_MARGIN_CHUNKS: i32 = (32 + 12 + 1) * 2;
47
48    /// Maximum valid chunk coordinate value.
49    /// Calculated as `SectionPos.blockToSectionCoord(MAX_HORIZONTAL_COORDINATE) - SAFETY_MARGIN_CHUNKS`.
50    pub const MAX_COORDINATE_VALUE: i32 =
51        SectionPos::block_to_section_coord(BlockPos::MAX_HORIZONTAL_COORDINATE)
52            - Self::SAFETY_MARGIN_CHUNKS;
53
54    /// Returns all 8 neighbors of this chunk position.
55    #[must_use]
56    pub fn neighbors(self) -> [ChunkPos; 8] {
57        Self::OFFSETS.map(|(dx, dy)| ChunkPos::new(self.0.x + dx, self.0.y + dy))
58    }
59
60    #[must_use]
61    #[inline]
62    /// Creates a new `ChunkPos` with the given x and y coordinates.
63    pub const fn new(x: i32, y: i32) -> Self {
64        Self(IVec2::new(x, y))
65    }
66
67    /// Creates a `ChunkPos` from a world block position.
68    #[must_use]
69    pub const fn from_block_pos(pos: BlockPos) -> Self {
70        Self::new(
71            SectionPos::block_to_section_coord(pos.0.x),
72            SectionPos::block_to_section_coord(pos.0.z),
73        )
74    }
75
76    /// Creates a `ChunkPos` containing the given floating-point world position.
77    #[must_use]
78    pub fn from_entity_pos(pos: DVec3) -> Self {
79        Self::from_block_pos(BlockPos::from(pos))
80    }
81
82    /// Checks if the given chunk coordinates are within valid bounds.
83    /// Uses `Mth.absMax(x, z) <= MAX_COORDINATE_VALUE`.
84    #[must_use]
85    #[inline]
86    pub const fn is_valid(x: i32, z: i32) -> bool {
87        x.abs().max(z.abs()) <= Self::MAX_COORDINATE_VALUE
88    }
89}
90
91impl WriteTo for ChunkPos {
92    fn write(&self, writer: &mut impl Write) -> io::Result<()> {
93        self.0.write(writer)
94    }
95}
96
97impl ReadFrom for ChunkPos {
98    fn read(data: &mut Cursor<&[u8]>) -> io::Result<Self> {
99        Ok(Self(IVec2::read(data)?))
100    }
101}
102
103/// A block position.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
105pub struct BlockPos(pub IVec3);
106
107/// Result of processing a node during bfs
108#[derive(Clone, Copy, Debug, PartialEq, Eq)]
109pub enum TraversalNodeStatus {
110    /// Count the node and visit its neighbors if depth allows
111    Accept,
112    /// Do not count the node or visit its neighbors
113    Skip,
114    /// Stop traversal immediately
115    Stop,
116}
117
118/// Iterator returned by [`BlockPos::spiral_around`].
119#[derive(Clone, Debug)]
120pub struct SpiralAround {
121    directions: [Direction; 4],
122    cursor: BlockPos,
123    legs: i32,
124    leg: i32,
125    leg_size: i32,
126    leg_index: i32,
127    last: BlockPos,
128}
129
130impl Iterator for SpiralAround {
131    type Item = BlockPos;
132
133    fn next(&mut self) -> Option<Self::Item> {
134        let direction_index = (self.leg + 4).rem_euclid(4) as usize;
135        self.cursor = self.last.relative(self.directions[direction_index]);
136        self.last = self.cursor;
137
138        if self.leg_index >= self.leg_size {
139            if self.leg >= self.legs {
140                return None;
141            }
142
143            self.leg += 1;
144            self.leg_index = 0;
145            self.leg_size = self.leg / 2 + 1;
146        }
147
148        self.leg_index += 1;
149        Some(self.cursor)
150    }
151}
152/// Iterator returned by [`BlockPos::between_closed`].
153#[derive(Debug, Clone)]
154pub struct BetweenClosed {
155    min_x: i32,
156    min_y: i32,
157    min_z: i32,
158    width: i32,
159    height: i32,
160    index: i32,
161    end: i32,
162}
163
164impl Iterator for BetweenClosed {
165    type Item = BlockPos;
166
167    fn next(&mut self) -> Option<Self::Item> {
168        if self.index == self.end {
169            return None;
170        }
171
172        let x = self.index % self.width;
173        let slice = self.index / self.width;
174        let y = slice % self.height;
175        let z = slice / self.height;
176        self.index += 1;
177
178        Some(BlockPos::new(
179            self.min_x + x,
180            self.min_y + y,
181            self.min_z + z,
182        ))
183    }
184
185    fn size_hint(&self) -> (usize, Option<usize>) {
186        let remaining = (self.end - self.index).max(0) as usize;
187        (remaining, Some(remaining))
188    }
189}
190impl From<DVec3> for BlockPos {
191    fn from(value: DVec3) -> Self {
192        BlockPos(IVec3 {
193            x: value.x.floor() as i32,
194            y: value.y.floor() as i32,
195            z: value.z.floor() as i32,
196        })
197    }
198}
199
200impl BlockPos {
201    pub const ZERO: BlockPos = BlockPos(IVec3::new(0, 0, 0));
202
203    /// Maximum horizontal coordinate value: `(1 << 26) / 2 - 1 = 33554431`
204    pub const MAX_HORIZONTAL_COORDINATE: i32 = (1 << PackedBlockPos::HORIZONTAL_BITS) / 2 - 1;
205
206    /// Creates a new `BlockPos` from coordinates.
207    #[must_use]
208    pub const fn new(x: i32, y: i32, z: i32) -> Self {
209        Self(IVec3::new(x, y, z))
210    }
211
212    /// Returns a new `BlockPos` offset by the given amounts.
213    #[must_use]
214    pub const fn offset(&self, dx: i32, dy: i32, dz: i32) -> Self {
215        Self(IVec3::new(self.0.x + dx, self.0.y + dy, self.0.z + dz))
216    }
217
218    /// Returns the x coordinate.
219    #[must_use]
220    pub const fn x(&self) -> i32 {
221        self.0.x
222    }
223
224    /// Returns the y coordinate.
225    #[must_use]
226    pub const fn y(&self) -> i32 {
227        self.0.y
228    }
229
230    /// Returns the z coordinate.
231    #[must_use]
232    pub const fn z(&self) -> i32 {
233        self.0.z
234    }
235
236    /// Returns vanilla `Vec3i.hashCode`, which orders Java `HashSet<BlockPos>` iteration.
237    #[must_use]
238    pub const fn java_hash_code(&self) -> i32 {
239        self.y()
240            .wrapping_add(self.z().wrapping_mul(31))
241            .wrapping_mul(31)
242            .wrapping_add(self.x())
243    }
244
245    /// Returns the position one block above (Y + 1).
246    #[must_use]
247    pub const fn above(&self) -> Self {
248        self.offset(0, 1, 0)
249    }
250
251    /// Returns the position `n` blocks above (Y + n).
252    #[must_use]
253    pub const fn above_n(&self, n: i32) -> Self {
254        self.offset(0, n, 0)
255    }
256
257    /// Returns the position one block below (Y - 1).
258    #[must_use]
259    pub const fn below(&self) -> Self {
260        self.offset(0, -1, 0)
261    }
262
263    /// Returns the position `n` blocks below (Y - n).
264    #[must_use]
265    pub const fn below_n(&self, n: i32) -> Self {
266        self.offset(0, -n, 0)
267    }
268
269    /// Returns the position one block to the north (Z - 1).
270    #[must_use]
271    pub const fn north(&self) -> Self {
272        self.offset(0, 0, -1)
273    }
274
275    /// Returns the position one block to the south (Z + 1).
276    #[must_use]
277    pub const fn south(&self) -> Self {
278        self.offset(0, 0, 1)
279    }
280
281    /// Returns the position one block to the west (X - 1).
282    #[must_use]
283    pub const fn west(&self) -> Self {
284        self.offset(-1, 0, 0)
285    }
286
287    /// Returns the position one block to the east (X + 1).
288    #[must_use]
289    pub const fn east(&self) -> Self {
290        self.offset(1, 0, 0)
291    }
292
293    /// Returns the position offset by one block in the given direction.
294    #[must_use]
295    pub fn relative(self, direction: Direction) -> Self {
296        Self(self.0 + direction.offset_vec())
297    }
298
299    /// Does a breadth-first traversal of all block pos from `start_pos`
300    #[must_use]
301    pub fn breadth_first_traversal<NP, P>(
302        start_pos: Self,
303        max_depth: i32,
304        max_count: i32,
305        mut neighbor_provider: NP,
306        mut node_processor: P,
307    ) -> i32
308    where
309        NP: FnMut(Self, &mut dyn FnMut(Self)),
310        P: FnMut(Self) -> TraversalNodeStatus,
311    {
312        let mut nodes = VecDeque::from([(start_pos, 0)]);
313        let mut visited = FxHashSet::default();
314        let mut count = 0;
315
316        while let Some((current_pos, depth)) = nodes.pop_front() {
317            if !visited.insert(current_pos) {
318                continue;
319            }
320
321            let next = node_processor(current_pos);
322            if next == TraversalNodeStatus::Skip {
323                continue;
324            }
325
326            if next == TraversalNodeStatus::Stop {
327                break;
328            }
329
330            count += 1;
331            if count >= max_count {
332                return count;
333            }
334
335            if depth < max_depth {
336                let next_depth = depth + 1;
337                neighbor_provider(current_pos, &mut |pos| nodes.push_back((pos, next_depth)));
338            }
339        }
340
341        count
342    }
343
344    /// Returns the position offset by `n` blocks in the given direction.
345    #[must_use]
346    pub fn relative_n(&self, direction: Direction, n: i32) -> Self {
347        if n == 0 {
348            *self
349        } else {
350            Self(self.0 + direction.offset_vec() * n)
351        }
352    }
353
354    /// Returns an iterator over positions in an outward square spiral around
355    /// `center`, alternating between `first_direction` and `second_direction`.
356    ///
357    /// # Panics
358    ///
359    /// Panics if `radius` is negative or if both directions are on the same axis.
360    #[must_use]
361    pub fn spiral_around(
362        center: Self,
363        radius: i32,
364        first_direction: Direction,
365        second_direction: Direction,
366    ) -> SpiralAround {
367        assert!(radius >= 0, "spiral radius must be non-negative");
368        assert!(
369            first_direction.get_axis() != second_direction.get_axis(),
370            "spiral directions cannot be on the same axis"
371        );
372
373        let cursor = center.relative(second_direction);
374        SpiralAround {
375            directions: [
376                first_direction,
377                second_direction,
378                first_direction.opposite(),
379                second_direction.opposite(),
380            ],
381            cursor,
382            legs: 4 * radius,
383            leg: -1,
384            leg_size: 0,
385            leg_index: 0,
386            last: cursor,
387        }
388    }
389
390    /// Returns the position offset by `n` blocks along the given axis.
391    #[must_use]
392    pub const fn relative_axis(&self, axis: Axis, n: i32) -> Self {
393        if n == 0 {
394            *self
395        } else {
396            match axis {
397                Axis::X => self.offset(n, 0, 0),
398                Axis::Y => self.offset(0, n, 0),
399                Axis::Z => self.offset(0, 0, n),
400            }
401        }
402    }
403
404    /// Returns a new position with the same X and Z but the given Y.
405    #[must_use]
406    pub const fn at_y(&self, y: i32) -> Self {
407        Self::new(self.0.x, y, self.0.z)
408    }
409
410    /// Returns a new position with all coordinates multiplied by the given factor.
411    #[must_use]
412    pub const fn multiply(&self, factor: i32) -> Self {
413        if factor == 1 {
414            *self
415        } else if factor == 0 {
416            Self::ZERO
417        } else {
418            Self::new(self.0.x * factor, self.0.y * factor, self.0.z * factor)
419        }
420    }
421
422    /// Returns the center of this block as a floating-point position.
423    #[must_use]
424    pub fn get_center(&self) -> (f64, f64, f64) {
425        (
426            f64::from(self.0.x) + 0.5,
427            f64::from(self.0.y) + 0.5,
428            f64::from(self.0.z) + 0.5,
429        )
430    }
431
432    /// Returns the bottom center of this block (center of the bottom face).
433    #[must_use]
434    pub fn get_bottom_center(&self) -> (f64, f64, f64) {
435        (
436            f64::from(self.0.x) + 0.5,
437            f64::from(self.0.y),
438            f64::from(self.0.z) + 0.5,
439        )
440    }
441
442    /// Returns whether this block's center is closer than `distance` to `position`.
443    #[must_use]
444    pub fn closer_to_center_than(&self, position: DVec3, distance: f64) -> bool {
445        let (x, y, z) = self.get_center();
446        DVec3::new(x, y, z).distance_squared(position) < distance * distance
447    }
448
449    /// Returns whether the squared integer distance to `other` is below `distance` squared.
450    #[must_use]
451    pub fn closer_than(&self, other: BlockPos, distance: f64) -> bool {
452        let dx = f64::from(self.0.x - other.0.x);
453        let dy = f64::from(self.0.y - other.0.y);
454        let dz = f64::from(self.0.z - other.0.z);
455        dx * dx + dy * dy + dz * dz < distance * distance
456    }
457
458    /// Creates a `BlockPos` containing the given floating-point coordinates.
459    #[must_use]
460    pub const fn containing(x: f64, y: f64, z: f64) -> Self {
461        Self::new(x.floor() as i32, y.floor() as i32, z.floor() as i32)
462    }
463
464    /// Returns the minimum coordinates of two positions.
465    #[must_use]
466    pub const fn min(a: BlockPos, b: BlockPos) -> Self {
467        Self::new(a.0.x.min(b.0.x), a.0.y.min(b.0.y), a.0.z.min(b.0.z))
468    }
469
470    /// Returns the maximum coordinates of two positions.
471    #[must_use]
472    pub const fn max(a: BlockPos, b: BlockPos) -> Self {
473        Self::new(a.0.x.max(b.0.x), a.0.y.max(b.0.y), a.0.z.max(b.0.z))
474    }
475
476    /// Returns positions in vanilla `BlockPos.withinManhattan` order.
477    #[must_use]
478    pub const fn within_manhattan(
479        self,
480        reach_x: i32,
481        reach_y: i32,
482        reach_z: i32,
483    ) -> BlockPosWithinManhattan {
484        BlockPosWithinManhattan {
485            origin: self,
486            reach_x,
487            reach_y,
488            reach_z,
489            max_depth: reach_x + reach_y + reach_z,
490            current_depth: 0,
491            max_x: 0,
492            max_y: 0,
493            x: 0,
494            y: 0,
495            pending_z_mirror: None,
496            done: false,
497        }
498    }
499    /// Returns vanilla `BlockPos.betweenClosed(BlockPos, BlockPos)`.
500    ///
501    /// Iterates all positions in the closed box spanned by `a` and `b`,
502    /// regardless of their relative min/max ordering.
503    #[must_use]
504    pub const fn between_closed(a: Self, b: Self) -> BetweenClosed {
505        Self::between_closed_coords(
506            a.0.x.min(b.0.x),
507            a.0.y.min(b.0.y),
508            a.0.z.min(b.0.z),
509            a.0.x.max(b.0.x),
510            a.0.y.max(b.0.y),
511            a.0.z.max(b.0.z),
512        )
513    }
514
515    /// Returns vanilla `BlockPos.betweenClosed(int, int, int, int, int, int)`.
516    ///
517    /// Iterates all positions in `[min_x, max_x] x [min_y, max_y] x [min_z, max_z]`.
518    #[must_use]
519    pub const fn between_closed_coords(
520        min_x: i32,
521        min_y: i32,
522        min_z: i32,
523        max_x: i32,
524        max_y: i32,
525        max_z: i32,
526    ) -> BetweenClosed {
527        let width = max_x - min_x + 1;
528        let height = max_y - min_y + 1;
529        let depth = max_z - min_z + 1;
530
531        BetweenClosed {
532            min_x,
533            min_y,
534            min_z,
535            width,
536            height,
537            index: 0,
538            end: width * height * depth,
539        }
540    }
541
542    /// Returns the closest position within the given search radii for which
543    /// `predicate` returns true, or `None` if no position matches.
544    #[must_use]
545    pub fn find_closest_match(
546        self,
547        horizontal_search_radius: i32,
548        vertical_search_radius: i32,
549        mut predicate: impl FnMut(BlockPos) -> bool,
550    ) -> Option<BlockPos> {
551        self.within_manhattan(
552            horizontal_search_radius,
553            vertical_search_radius,
554            horizontal_search_radius,
555        )
556        .find(|pos| predicate(*pos))
557    }
558}
559
560/// Iterator returned by [`BlockPos::within_manhattan`].
561#[derive(Debug, Clone)]
562pub struct BlockPosWithinManhattan {
563    origin: BlockPos,
564    reach_x: i32,
565    reach_y: i32,
566    reach_z: i32,
567    max_depth: i32,
568    current_depth: i32,
569    max_x: i32,
570    max_y: i32,
571    x: i32,
572    y: i32,
573    pending_z_mirror: Option<BlockPos>,
574    done: bool,
575}
576
577impl Iterator for BlockPosWithinManhattan {
578    type Item = BlockPos;
579
580    fn next(&mut self) -> Option<Self::Item> {
581        if let Some(pos) = self.pending_z_mirror.take() {
582            return Some(pos);
583        }
584        if self.done {
585            return None;
586        }
587
588        loop {
589            if self.y > self.max_y {
590                self.x += 1;
591                if self.x > self.max_x {
592                    self.current_depth += 1;
593                    if self.current_depth > self.max_depth {
594                        self.done = true;
595                        return None;
596                    }
597
598                    self.max_x = self.reach_x.min(self.current_depth);
599                    self.x = -self.max_x;
600                }
601
602                self.max_y = self.reach_y.min(self.current_depth - self.x.abs());
603                self.y = -self.max_y;
604            }
605
606            let x = self.x;
607            let y = self.y;
608            let z = self.current_depth - x.abs() - y.abs();
609            self.y += 1;
610            if z > self.reach_z {
611                continue;
612            }
613
614            let pos = self.origin.offset(x, y, z);
615            if z != 0 {
616                self.pending_z_mirror = Some(self.origin.offset(x, y, -z));
617            }
618            return Some(pos);
619        }
620    }
621}
622
623impl ReadFrom for BlockPos {
624    fn read(data: &mut Cursor<&[u8]>) -> io::Result<Self> {
625        let packed = <i64 as ReadFrom>::read(data)?;
626        Ok(PackedBlockPos::from_raw(packed).into())
627    }
628}
629
630/// A position tied to a dimension key.
631#[derive(Debug, Clone, PartialEq, Eq, Hash)]
632pub struct GlobalPos {
633    /// Dimension containing the block position.
634    pub dimension: Identifier,
635    /// Block position within the dimension.
636    pub pos: BlockPos,
637}
638
639impl GlobalPos {
640    /// Creates a new global position.
641    #[must_use]
642    pub const fn new(dimension: Identifier, pos: BlockPos) -> Self {
643        Self { dimension, pos }
644    }
645}
646
647impl ReadFrom for GlobalPos {
648    fn read(data: &mut Cursor<&[u8]>) -> io::Result<Self> {
649        Ok(Self {
650            dimension: <Identifier as ReadFrom>::read(data)?,
651            pos: BlockPos::read(data)?,
652        })
653    }
654}
655
656impl WriteTo for GlobalPos {
657    fn write(&self, writer: &mut impl Write) -> io::Result<()> {
658        self.dimension.write(writer)?;
659        self.pos.write(writer)
660    }
661}
662
663/// A chunk section position (16x16x16 region).
664#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
665pub struct SectionPos(pub IVec3);
666
667impl SectionPos {
668    const SECTION_BITS: i32 = 4;
669    const SECTION_SIZE: i32 = 1 << Self::SECTION_BITS; // 16
670    pub(super) const SECTION_MASK: i32 = Self::SECTION_SIZE - 1; // 15
671
672    /// Creates a new `SectionPos` from section coordinates.
673    #[must_use]
674    pub const fn new(x: i32, y: i32, z: i32) -> Self {
675        Self(IVec3::new(x, y, z))
676    }
677
678    /// Converts a block coordinate to a section coordinate.
679    #[must_use]
680    #[inline]
681    pub const fn block_to_section_coord(block_coord: i32) -> i32 {
682        block_coord >> Self::SECTION_BITS
683    }
684
685    /// Creates a `SectionPos` from a `BlockPos`.
686    #[must_use]
687    pub const fn from_block_pos(pos: BlockPos) -> Self {
688        Self::new(
689            Self::block_to_section_coord(pos.0.x),
690            Self::block_to_section_coord(pos.0.y),
691            Self::block_to_section_coord(pos.0.z),
692        )
693    }
694
695    /// Creates a `SectionPos` containing the given floating-point world position.
696    #[must_use]
697    pub fn from_entity_pos(pos: DVec3) -> Self {
698        Self::from_block_pos(BlockPos::from(pos))
699    }
700
701    /// Gets the X coordinate.
702    #[must_use]
703    pub const fn x(&self) -> i32 {
704        self.0.x
705    }
706
707    /// Gets the Y coordinate.
708    #[must_use]
709    pub const fn y(&self) -> i32 {
710        self.0.y
711    }
712
713    /// Gets the Z coordinate.
714    #[must_use]
715    pub const fn z(&self) -> i32 {
716        self.0.z
717    }
718
719    /// Converts section-relative coordinates to an absolute block X coordinate.
720    #[must_use]
721    pub const fn relative_to_block_x(&self, relative: PackedSectionBlockPos) -> i32 {
722        (self.0.x << Self::SECTION_BITS) + relative.x() as i32
723    }
724
725    /// Converts section-relative coordinates to an absolute block Y coordinate.
726    #[must_use]
727    pub const fn relative_to_block_y(&self, relative: PackedSectionBlockPos) -> i32 {
728        (self.0.y << Self::SECTION_BITS) + relative.y() as i32
729    }
730
731    /// Converts section-relative coordinates to an absolute block Z coordinate.
732    #[must_use]
733    pub const fn relative_to_block_z(&self, relative: PackedSectionBlockPos) -> i32 {
734        (self.0.z << Self::SECTION_BITS) + relative.z() as i32
735    }
736
737    /// Packs a block position into a section-relative offset.
738    /// Format: (x << 8) | (z << 4) | y (each coordinate masked to 4 bits)
739    #[must_use]
740    #[inline]
741    pub const fn section_relative_pos(pos: BlockPos) -> PackedSectionBlockPos {
742        PackedSectionBlockPos::from_block_pos(pos)
743    }
744
745    /// Converts a section-relative packed position back to a block position.
746    #[must_use]
747    pub const fn relative_to_block_pos(&self, relative: PackedSectionBlockPos) -> BlockPos {
748        BlockPos(IVec3::new(
749            self.relative_to_block_x(relative),
750            self.relative_to_block_y(relative),
751            self.relative_to_block_z(relative),
752        ))
753    }
754}
755
756impl ReadFrom for SectionPos {
757    fn read(data: &mut Cursor<&[u8]>) -> io::Result<Self> {
758        Ok(<PackedSectionPos as ReadFrom>::read(data)?.into())
759    }
760}
761
762impl WriteTo for SectionPos {
763    fn write(&self, writer: &mut impl Write) -> io::Result<()> {
764        PackedSectionPos::from(*self).write(writer)
765    }
766}
767
768#[cfg(test)]
769mod tests {
770    use glam::DVec3;
771
772    use super::BlockPos;
773
774    #[test]
775    fn closer_to_center_than_uses_block_center() {
776        let pos = BlockPos::new(0, 0, 0);
777        assert!(pos.closer_to_center_than(DVec3::new(0.5, 0.5, 0.5), 1.0));
778        assert!(!pos.closer_to_center_than(DVec3::new(1.5, 0.5, 0.5), 1.0));
779    }
780
781    #[test]
782    fn closer_than_compares_integer_offsets_strictly() {
783        let pos = BlockPos::new(0, 64, 0);
784        assert!(pos.closer_than(BlockPos::new(3, 64, 3), 5.0));
785        assert!(!pos.closer_than(BlockPos::new(3, 64, 4), 5.0));
786        assert!(pos.closer_than(BlockPos::new(-3, 60, 0), 5.1));
787    }
788}