maplibre/legacy/
collision_index.rs

1//! Translated from https://github.com/maplibre/maplibre-native/blob/4add9ea/src/mbgl/text/collision_index.cpp
2
3use std::collections::HashMap;
4
5use bitflags::bitflags;
6use cgmath::{Matrix4, Vector4};
7
8use crate::{
9    coords::EXTENT,
10    euclid::{Box2D, Point2D},
11    legacy::{
12        buckets::symbol_bucket::PlacedSymbol,
13        collision_feature::{CollisionBox, CollisionFeature, ProjectedCollisionBox},
14        geometry::feature_index::IndexedSubfeature,
15        grid_index::{Circle, GridIndex},
16        layout::symbol_projection::{place_first_and_last_glyph, project, TileDistance},
17        util::geo::ScreenLineString,
18        MapMode, ScreenSpace, TileSpace,
19    },
20    render::{camera::ModelViewProjection, view_state::ViewState},
21};
22
23/// maplibre/maplibre-native#4add9ea original name: TransformState
24type TransformState = ViewState;
25
26/// maplibre/maplibre-native#4add9ea original name: CollisionBoundaries
27type CollisionBoundaries = Box2D<f64, ScreenSpace>; // [f64; 4]; // [x1, y1, x2, y2]
28
29bitflags! {
30    /// Represents a set of flags.
31    /// maplibre/maplibre-native#4add9ea original name: IntersectStatusFlags:
32    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
33    struct IntersectStatusFlags: u8 {
34        const None = 0;
35        const HorizontalBorders = 1 << 0;
36        const VerticalBorders = 1 << 1;
37
38    }
39}
40
41impl Default for IntersectStatusFlags {
42    /// maplibre/maplibre-native#4add9ea original name: default
43    fn default() -> Self {
44        Self::None
45    }
46}
47
48/// maplibre/maplibre-native#4add9ea original name: IntersectStatus
49#[derive(Default)]
50struct IntersectStatus {
51    flags: IntersectStatusFlags,
52    // Assuming tile border divides box in two sections
53    min_section_length: i32,
54}
55
56// When a symbol crosses the edge that causes it to be included in
57// collision detection, it will cause changes in the symbols around
58// it. This ant specifies how many pixels to pad the edge of
59// the viewport for collision detection so that the bulk of the changes
60// occur offscreen. Making this ant greater increases label
61// stability, but it's expensive.
62// TODO remove const viewportPaddingDefault: f64 = -10.;
63const VIEWPORT_PADDING_DEFAULT: f64 = 100.;
64// Viewport padding must be much larger for static tiles to avoid clipped labels.
65const VIEWPORT_PADDING_FOR_STATIC_TILES: f64 = 1024.;
66
67/// maplibre/maplibre-native#4add9ea original name: findViewportPadding
68fn find_viewport_padding(transform_state: &TransformState, map_mode: MapMode) -> f64 {
69    if map_mode == MapMode::Tile {
70        return VIEWPORT_PADDING_FOR_STATIC_TILES;
71    }
72    if transform_state.camera().get_pitch().0 != 0.0 {
73        VIEWPORT_PADDING_DEFAULT * 2.
74    } else {
75        VIEWPORT_PADDING_DEFAULT
76    }
77}
78
79/// maplibre/maplibre-native#4add9ea original name: CollisionGrid
80type CollisionGrid = GridIndex<IndexedSubfeature>;
81/// maplibre/maplibre-native#4add9ea original name: CollisionIndex
82pub struct CollisionIndex {
83    transform_state: TransformState,
84    viewport_padding: f64,
85    collision_grid: CollisionGrid,
86    ignored_grid: CollisionGrid,
87    screen_right_boundary: f64,
88    screen_bottom_boundary: f64,
89    grid_right_boundary: f64,
90    grid_bottom_boundary: f64,
91    pitch_factor: f64,
92}
93
94impl CollisionIndex {
95    /// maplibre/maplibre-native#4add9ea original name: new
96    pub fn new(transform_state: &TransformState, map_mode: MapMode) -> Self {
97        let viewport_padding = find_viewport_padding(transform_state, map_mode);
98        Self {
99            transform_state: transform_state.clone(),
100            viewport_padding,
101            collision_grid: CollisionGrid::new(
102                transform_state.width() + 2. * viewport_padding,
103                transform_state.height() + 2. * viewport_padding,
104                25,
105            ),
106            ignored_grid: CollisionGrid::new(
107                transform_state.width() + 2. * viewport_padding,
108                transform_state.height() + 2. * viewport_padding,
109                25,
110            ),
111            screen_right_boundary: transform_state.width() + viewport_padding,
112            screen_bottom_boundary: transform_state.height() + viewport_padding,
113            grid_right_boundary: transform_state.width() + 2. * viewport_padding,
114            grid_bottom_boundary: transform_state.height() + 2. * viewport_padding,
115            pitch_factor: transform_state.camera().get_pitch().0.cos()
116                * transform_state.camera_to_center_distance(),
117        }
118    }
119
120    /// maplibre/maplibre-native#4add9ea original name: intersectsTileEdges
121    pub fn intersects_tile_edges(
122        &self,
123        box_: &CollisionBox,
124        shift: Point2D<f64, ScreenSpace>,
125        pos_matrix: &ModelViewProjection,
126        text_pixel_ratio: f64,
127        tile_edges: CollisionBoundaries,
128    ) -> IntersectStatus {
129        let boundaries =
130            self.get_projected_collision_boundaries(pos_matrix, shift, text_pixel_ratio, box_);
131        let mut result: IntersectStatus = IntersectStatus::default();
132        let x1 = boundaries.min.x;
133        let y1 = boundaries.min.y;
134        let x2 = boundaries.max.x;
135        let y2 = boundaries.max.y;
136
137        let tile_x1 = tile_edges.min.x;
138        let tile_y1 = tile_edges.min.y;
139        let tile_x2 = tile_edges.max.x;
140        let tile_y2 = tile_edges.max.y;
141
142        // Check left border
143        let mut min_section_length = ((tile_x1 - x1).min(x2 - tile_x1)) as i32;
144        if min_section_length <= 0 {
145            // Check right border
146            min_section_length = ((tile_x2 - x1).min(x2 - tile_x2)) as i32;
147        }
148        if min_section_length > 0 {
149            result.flags |= IntersectStatusFlags::VerticalBorders;
150            result.min_section_length = min_section_length;
151        }
152        // Check top border
153        min_section_length = ((tile_y1 - y1).min(y2 - tile_y1)) as i32;
154        if min_section_length <= 0 {
155            // Check bottom border
156            min_section_length = ((tile_y2 - y1).min(y2 - tile_y2)) as i32;
157        }
158        if min_section_length > 0 {
159            result.flags |= IntersectStatusFlags::HorizontalBorders;
160            result.min_section_length = (result.min_section_length).min(min_section_length);
161        }
162        result
163    }
164
165    /// maplibre/maplibre-native#4add9ea original name: placeFeature
166    pub fn place_feature<F>(
167        &self,
168        feature: &CollisionFeature,
169        shift: Point2D<f64, ScreenSpace>,
170        pos_matrix: &ModelViewProjection,
171        label_plane_matrix: &Matrix4<f64>,
172        text_pixel_ratio: f64,
173        symbol: &PlacedSymbol,
174        scale: f64,
175        font_size: f64,
176        allow_overlap: bool,
177        pitch_with_map: bool,
178        collision_debug: bool,
179        avoid_edges: Option<CollisionBoundaries>,
180        collision_group_predicate: Option<F>,
181        projected_boxes: &mut Vec<ProjectedCollisionBox>, /*out*/
182    ) -> (bool, bool)
183    where
184        F: Fn(&IndexedSubfeature) -> bool,
185    {
186        assert!(projected_boxes.is_empty());
187        if !feature.along_line {
188            let box_ = feature.boxes.first().unwrap();
189            let collision_boundaries =
190                self.get_projected_collision_boundaries(pos_matrix, shift, text_pixel_ratio, box_);
191            projected_boxes.push(ProjectedCollisionBox::Box(collision_boundaries));
192
193            if let Some(avoid_edges) = avoid_edges {
194                if !self.is_inside_tile(&collision_boundaries, &avoid_edges) {
195                    return (false, false);
196                }
197            }
198
199            if !self.is_inside_grid(&collision_boundaries)
200                || (!allow_overlap
201                    && self.collision_grid.hit_test(
202                        projected_boxes.last().unwrap().box_(),
203                        collision_group_predicate,
204                    ))
205            {
206                return (false, false);
207            }
208
209            (true, self.is_offscreen(&collision_boundaries))
210        } else {
211            self.place_line_feature(
212                feature,
213                pos_matrix,
214                label_plane_matrix,
215                text_pixel_ratio,
216                symbol,
217                scale,
218                font_size,
219                allow_overlap,
220                pitch_with_map,
221                collision_debug,
222                avoid_edges,
223                collision_group_predicate,
224                projected_boxes,
225            )
226        }
227    }
228
229    /// maplibre/maplibre-native#4add9ea original name: insertFeature
230    pub fn insert_feature(
231        &mut self,
232        feature: CollisionFeature,
233        projected_boxes: &Vec<ProjectedCollisionBox>,
234        ignore_placement: bool,
235        bucket_instance_id: u32,
236        collision_group_id: u16,
237    ) {
238        if feature.along_line {
239            for circle in projected_boxes {
240                if !circle.is_circle() {
241                    continue;
242                }
243
244                if ignore_placement {
245                    self.ignored_grid.insert_circle(
246                        IndexedSubfeature::new(
247                            feature.indexed_feature.clone(),
248                            bucket_instance_id,
249                            collision_group_id,
250                        ), // FIXME clone() should not be needed?
251                        *circle.circle(),
252                    );
253                } else {
254                    self.collision_grid.insert_circle(
255                        IndexedSubfeature::new(
256                            feature.indexed_feature.clone(),
257                            bucket_instance_id,
258                            collision_group_id,
259                        ), // FIXME clone() should not be needed?
260                        *circle.circle(),
261                    );
262                }
263            }
264        } else if !projected_boxes.is_empty() {
265            assert!(projected_boxes.len() == 1);
266            let box_ = projected_boxes[0];
267            // TODO assert!(box_.isBox());
268            if ignore_placement {
269                self.ignored_grid.insert(
270                    IndexedSubfeature::new(
271                        feature.indexed_feature,
272                        bucket_instance_id,
273                        collision_group_id,
274                    ),
275                    *box_.box_(),
276                );
277            } else {
278                self.collision_grid.insert(
279                    IndexedSubfeature::new(
280                        feature.indexed_feature,
281                        bucket_instance_id,
282                        collision_group_id,
283                    ),
284                    *box_.box_(),
285                );
286            }
287        }
288    }
289
290    /// maplibre/maplibre-native#4add9ea original name: queryRenderedSymbols
291    pub fn query_rendered_symbols(
292        &self,
293        line_string: &ScreenLineString,
294    ) -> HashMap<u32, Vec<IndexedSubfeature>> {
295        todo!()
296    }
297
298    /// maplibre/maplibre-native#4add9ea original name: projectTileBoundaries
299    pub fn project_tile_boundaries(&self, pos_matrix: &ModelViewProjection) -> CollisionBoundaries {
300        let top_left = self.project_point(pos_matrix, &Point2D::zero());
301        let bottom_right = self.project_point(pos_matrix, &Point2D::new(EXTENT, EXTENT)); // FIXME: maplibre-native uses here 8192 for extent
302
303        CollisionBoundaries::new(
304            Point2D::new(top_left.x, top_left.y),
305            Point2D::new(bottom_right.x, bottom_right.y),
306        )
307    }
308
309    /// maplibre/maplibre-native#4add9ea original name: getTransformState
310    pub fn get_transform_state(&self) -> &TransformState {
311        &self.transform_state
312    }
313
314    /// maplibre/maplibre-native#4add9ea original name: getViewportPadding
315    pub fn get_viewport_padding(&self) -> f64 {
316        self.viewport_padding
317    }
318}
319
320impl CollisionIndex {
321    /// maplibre/maplibre-native#4add9ea original name: isOffscreen
322    fn is_offscreen(&self, boundaries: &CollisionBoundaries) -> bool {
323        boundaries.max.x < self.viewport_padding
324            || boundaries.min.x >= self.screen_right_boundary
325            || boundaries.max.y < self.viewport_padding
326            || boundaries.min.y >= self.screen_bottom_boundary
327    }
328    /// maplibre/maplibre-native#4add9ea original name: isInsideGrid
329    fn is_inside_grid(&self, boundaries: &CollisionBoundaries) -> bool {
330        boundaries.max.x >= 0.
331            && boundaries.min.x < self.grid_right_boundary
332            && boundaries.max.y >= 0.
333            && boundaries.min.y < self.grid_bottom_boundary
334    }
335
336    /// maplibre/maplibre-native#4add9ea original name: isInsideTile
337    fn is_inside_tile(
338        &self,
339        boundaries: &CollisionBoundaries,
340        tile_boundaries: &CollisionBoundaries,
341    ) -> bool {
342        boundaries.min.x >= tile_boundaries.min.x
343            && boundaries.min.y >= tile_boundaries.min.y
344            && boundaries.max.x < tile_boundaries.max.x
345            && boundaries.max.y < tile_boundaries.max.y
346    }
347
348    /// maplibre/maplibre-native#4add9ea original name: overlapsTile
349    fn overlaps_tile(
350        &self,
351        boundaries: &CollisionBoundaries,
352        tile_boundaries: &CollisionBoundaries,
353    ) -> bool {
354        boundaries.min.x < tile_boundaries.max.x
355            && boundaries.max.x > tile_boundaries.min.x
356            && boundaries.min.y < tile_boundaries.max.y
357            && boundaries.max.y > tile_boundaries.min.y
358    }
359
360    /// maplibre/maplibre-native#4add9ea original name: placeLineFeature
361    fn place_line_feature<F>(
362        &self,
363        feature: &CollisionFeature,
364        pos_matrix: &ModelViewProjection,
365        label_plane_matrix: &Matrix4<f64>,
366        text_pixel_ratio: f64,
367        symbol: &PlacedSymbol,
368        scale: f64,
369        font_size: f64,
370        allow_overlap: bool,
371        pitch_with_map: bool,
372        collision_debug: bool,
373        avoid_edges: Option<CollisionBoundaries>,
374        collision_group_predicate: Option<F>,
375        projected_boxes: &mut Vec<ProjectedCollisionBox>, /*out*/
376    ) -> (bool, bool)
377    where
378        F: Fn(&IndexedSubfeature) -> bool,
379    {
380        assert!(feature.along_line);
381        assert!(projected_boxes.is_empty());
382        let tile_unit_anchor_point = symbol.anchor_point;
383        let projected_anchor = self.project_anchor(pos_matrix, &tile_unit_anchor_point);
384
385        let font_scale = font_size / 24.;
386        let line_offset_x = symbol.line_offset[0] * font_size;
387        let line_offset_y = symbol.line_offset[1] * font_size;
388
389        let label_plane_anchor_point = project(tile_unit_anchor_point, label_plane_matrix).0;
390
391        let first_and_last_glyph = place_first_and_last_glyph(
392            font_scale,
393            line_offset_x,
394            line_offset_y,
395            /*flip*/ false,
396            label_plane_anchor_point,
397            tile_unit_anchor_point,
398            symbol,
399            label_plane_matrix,
400            /*return tile distance*/ true,
401        );
402
403        let mut collision_detected = false;
404        let mut in_grid = false;
405        let mut entirely_offscreen = true;
406
407        let tile_to_viewport = projected_anchor.0 * text_pixel_ratio;
408        // pixelsToTileUnits is used for translating line geometry to tile units
409        // ... so we care about 'scale' but not 'perspectiveRatio'
410        // equivalent to pixel_to_tile_units
411        let pixels_to_tile_units = 1. / (text_pixel_ratio * scale);
412
413        let mut first_tile_distance = 0.;
414        let mut last_tile_distance = 0.;
415        if let Some(first_and_last_glyph) = &first_and_last_glyph {
416            first_tile_distance = self.approximate_tile_distance(
417                first_and_last_glyph.0.tile_distance.as_ref().unwrap(),
418                first_and_last_glyph.0.angle,
419                pixels_to_tile_units,
420                projected_anchor.1,
421                pitch_with_map,
422            );
423            last_tile_distance = self.approximate_tile_distance(
424                first_and_last_glyph.1.tile_distance.as_ref().unwrap(),
425                first_and_last_glyph.1.angle,
426                pixels_to_tile_units,
427                projected_anchor.1,
428                pitch_with_map,
429            );
430        }
431
432        let mut previous_circle_placed = false;
433        projected_boxes.resize(feature.boxes.len(), ProjectedCollisionBox::default());
434        for i in 0..feature.boxes.len() {
435            let circle = feature.boxes[i];
436            let box_signed_distance_from_anchor = circle.signed_distance_from_anchor;
437            if first_and_last_glyph.is_none()
438                || (box_signed_distance_from_anchor < -first_tile_distance)
439                || (box_signed_distance_from_anchor > last_tile_distance)
440            {
441                // The label either doesn't fit on its line or we
442                // don't need to use this circle because the label
443                // doesn't extend this far. Either way, mark the circle unused.
444                previous_circle_placed = false;
445                continue;
446            }
447
448            let projected_point = self.project_point(pos_matrix, &circle.anchor);
449            let tile_unit_radius = (circle.x2 - circle.x1) / 2.;
450            let radius = tile_unit_radius * tile_to_viewport;
451
452            if previous_circle_placed {
453                let previous_circle = &projected_boxes[i - 1];
454                assert!(previous_circle.is_circle());
455                let previous_center = previous_circle.circle().center;
456                let dx = projected_point.x - previous_center.x;
457                let dy = projected_point.y - previous_center.y;
458                // The circle edges touch when the distance between their centers is
459                // 2x the radius When the distance is 1x the radius, they're doubled
460                // up, and we could remove every other circle while keeping them all
461                // in touch. We actually start removing circles when the distance is
462                // √2x the radius:
463                //  thinning the number of circles as much as possible is a major
464                //  performance win, and the small gaps introduced don't make a very
465                //  noticeable difference.
466                let placed_too_densely = radius * radius * 2. > dx * dx + dy * dy;
467                if placed_too_densely {
468                    let at_least_one_more_circle = (i + 1) < feature.boxes.len();
469                    if at_least_one_more_circle {
470                        let next_circle = feature.boxes[i + 1];
471                        let next_box_distance_from_anchor = next_circle.signed_distance_from_anchor;
472                        if (next_box_distance_from_anchor > -first_tile_distance)
473                            && (next_box_distance_from_anchor < last_tile_distance)
474                        {
475                            // Hide significantly overlapping circles, unless this
476                            // is the last one we can use, in which case we want to
477                            // keep it in place even if it's tightly packed with the
478                            // one before it.
479                            previous_circle_placed = false;
480                            continue;
481                        }
482                    }
483                }
484            }
485
486            previous_circle_placed = true;
487
488            let collision_boundaries = CollisionBoundaries::new(
489                Point2D::new(projected_point.x - radius, projected_point.y - radius),
490                Point2D::new(projected_point.x + radius, projected_point.y + radius),
491            );
492
493            projected_boxes[i] = ProjectedCollisionBox::Circle(Circle::new(
494                Point2D::new(projected_point.x, projected_point.y),
495                radius,
496            ));
497
498            entirely_offscreen &= self.is_offscreen(&collision_boundaries);
499            in_grid |= self.is_inside_grid(&collision_boundaries);
500
501            if let Some(avoid_edges) = avoid_edges {
502                if !self.is_inside_tile(&collision_boundaries, &avoid_edges) {
503                    if !collision_debug {
504                        return (false, false);
505                    } else {
506                        // Don't early exit if we're showing the debug circles because
507                        // we still want to calculate which circles are in use
508                        collision_detected = true;
509                    }
510                }
511            }
512
513            if !allow_overlap
514                && self.collision_grid.hit_test_circle(
515                    projected_boxes[i].circle(),
516                    collision_group_predicate.as_ref(),
517                )
518            {
519                if !collision_debug {
520                    return (false, false);
521                } else {
522                    // Don't early exit if we're showing the debug circles because
523                    // we still want to calculate which circles are in use
524                    collision_detected = true;
525                }
526            }
527        }
528
529        (
530            !collision_detected && first_and_last_glyph.is_some() && in_grid,
531            entirely_offscreen,
532        )
533    }
534
535    /// maplibre/maplibre-native#4add9ea original name: approximateTileDistance
536    fn approximate_tile_distance(
537        &self,
538        tile_distance: &TileDistance,
539        last_segment_angle: f64,
540        pixels_to_tile_units: f64,
541        camera_to_anchor_distance: f64,
542        pitch_with_map: bool,
543    ) -> f64 {
544        // This is a quick and dirty solution for chosing which collision circles to
545        // use (since collision circles are laid out in tile units). Ideally, I
546        // think we should generate collision circles on the fly in viewport
547        // coordinates at the time we do collision detection.
548
549        // incidenceStretch is the ratio of how much y space a label takes up on a
550        // tile while drawn perpendicular to the viewport vs
551        //  how much space it would take up if it were drawn flat on the tile
552        // Using law of sines, camera_to_anchor/sin(ground_angle) =
553        // camera_to_center/sin(incidence_angle) Incidence angle 90 -> head on,
554        // sin(incidence_angle) = 1, no stretch Incidence angle 1 -> very oblique,
555        // sin(incidence_angle) =~ 0, lots of stretch ground_angle = u_pitch + PI/2
556        // -> sin(ground_angle) = cos(u_pitch) incidenceStretch = 1 /
557        // sin(incidenceAngle)
558
559        let incidence_stretch = if pitch_with_map {
560            1.
561        } else {
562            camera_to_anchor_distance / self.pitch_factor
563        };
564        let last_segment_tile = tile_distance.last_segment_viewport_distance * pixels_to_tile_units;
565        tile_distance.prev_tile_distance
566            + last_segment_tile
567            + (incidence_stretch - 1.) * last_segment_tile * last_segment_angle.sin().abs()
568    }
569
570    /// maplibre/maplibre-native#4add9ea original name: projectAnchor
571    fn project_anchor(
572        &self,
573        pos_matrix: &ModelViewProjection,
574        point: &Point2D<f64, TileSpace>,
575    ) -> (f64, f64) {
576        let p = Vector4::new(point.x, point.y, 0., 1.);
577        let p = pos_matrix.project(p); // TODO verify multiplication
578        (
579            0.5 + 0.5 * (self.transform_state.camera_to_center_distance() / p[3]),
580            p[3],
581        )
582    }
583    /// maplibre/maplibre-native#4add9ea original name: projectAndGetPerspectiveRatio
584    fn project_and_get_perspective_ratio(
585        &self,
586        pos_matrix: &ModelViewProjection,
587        point: &Point2D<f64, TileSpace>,
588    ) -> (Point2D<f64, ScreenSpace>, f64) {
589        let p = Vector4::new(point.x, point.y, 0., 1.);
590        let p = pos_matrix.project(p); // TODO verify multiplication
591        let width = self.transform_state.width();
592        let height = self.transform_state.height();
593        let ccd = self.transform_state.camera_to_center_distance();
594        (
595            Point2D::new(
596                ((p[0] / p[3] + 1.) / 2.) * width + self.viewport_padding,
597                ((-p[1] / p[3] + 1.) / 2.) * height + self.viewport_padding,
598            ),
599            // See perspective ratio comment in symbol_sdf.vertex
600            // We're doing collision detection in viewport space so we need
601            // to scale down boxes in the distance
602            0.5 + 0.5 * ccd / p[3],
603        )
604    }
605    /// maplibre/maplibre-native#4add9ea original name: projectPoint
606    fn project_point(
607        &self,
608        pos_matrix: &ModelViewProjection,
609        point: &Point2D<f64, TileSpace>,
610    ) -> Point2D<f64, ScreenSpace> {
611        let p = Vector4::new(point.x, point.y, 0., 1.);
612        let p = pos_matrix.project(p); // TODO verify multiplication
613        let width = self.transform_state.width();
614        let height = self.transform_state.height();
615        Point2D::new(
616            ((p[0] / p[3] + 1.) / 2.) * width + self.viewport_padding,
617            ((-p[1] / p[3] + 1.) / 2.) * height + self.viewport_padding,
618        )
619    }
620
621    /// maplibre/maplibre-native#4add9ea original name: getProjectedCollisionBoundaries
622    fn get_projected_collision_boundaries(
623        &self,
624        pos_matrix: &ModelViewProjection,
625        shift: Point2D<f64, ScreenSpace>,
626        text_pixel_ratio: f64,
627        box_: &CollisionBox,
628    ) -> CollisionBoundaries {
629        let (projected_point, tile_to_viewport) =
630            self.project_and_get_perspective_ratio(pos_matrix, &box_.anchor);
631        let tile_to_viewport = text_pixel_ratio * tile_to_viewport;
632        let tile_to_viewport = 1.; // TODO
633        CollisionBoundaries::new(
634            Point2D::new(
635                (box_.x1 + shift.x) * tile_to_viewport + projected_point.x,
636                (box_.y1 + shift.y) * tile_to_viewport + projected_point.y,
637            ),
638            Point2D::new(
639                (box_.x2 + shift.x) * tile_to_viewport + projected_point.x,
640                (box_.y2 + shift.y) * tile_to_viewport + projected_point.y,
641            ),
642        )
643    }
644}