mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 17:23:25 +00:00
fix: refine map controls and garage layout
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
defaultCayoStyle,
|
||||
defaultMainlandStyle,
|
||||
defaultMapPercentToWorld,
|
||||
defaultMapWorldToPercent,
|
||||
} from '@/features/map/defaultMapGeometry'
|
||||
@@ -13,4 +15,18 @@ describe('default map geometry', () => {
|
||||
expect(restored.x).toBeCloseTo(world.x, 6)
|
||||
expect(restored.y).toBeCloseTo(world.y, 6)
|
||||
})
|
||||
|
||||
it('keeps every rendered map layer inside the composite canvas', () => {
|
||||
for (const style of [defaultMainlandStyle, defaultCayoStyle]) {
|
||||
const left = Number.parseFloat(style.left)
|
||||
const top = Number.parseFloat(style.top)
|
||||
const width = Number.parseFloat(style.width)
|
||||
const height = Number.parseFloat(style.height)
|
||||
|
||||
expect(left).toBeGreaterThanOrEqual(0)
|
||||
expect(top).toBeGreaterThanOrEqual(0)
|
||||
expect(left + width).toBeLessThanOrEqual(100.000001)
|
||||
expect(top + height).toBeLessThanOrEqual(100.000001)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,19 +15,29 @@ const cayoMapCoordinates = {
|
||||
height: 1994.4,
|
||||
}
|
||||
|
||||
const cayoTerritoryBounds = {
|
||||
maxX: 7542.07,
|
||||
minY: -7170.07,
|
||||
const cayoLayerBounds = {
|
||||
minX: cayoMapCoordinates.centerX - cayoMapCoordinates.width / 2,
|
||||
minY:
|
||||
defaultMainlandCoordinates.yFlipOffset -
|
||||
(cayoMapCoordinates.centerY + cayoMapCoordinates.height / 2),
|
||||
width: cayoMapCoordinates.width,
|
||||
height: cayoMapCoordinates.height,
|
||||
}
|
||||
|
||||
const mapMaximumX = Math.max(
|
||||
defaultMainlandCoordinates.minX + defaultMainlandCoordinates.width,
|
||||
cayoLayerBounds.minX + cayoLayerBounds.width,
|
||||
)
|
||||
const mapMaximumY = Math.max(
|
||||
defaultMainlandCoordinates.minY + defaultMainlandCoordinates.height,
|
||||
cayoLayerBounds.minY + cayoLayerBounds.height,
|
||||
)
|
||||
|
||||
export const defaultMapCoordinates = {
|
||||
minX: defaultMainlandCoordinates.minX,
|
||||
minY: defaultMainlandCoordinates.minY,
|
||||
width: cayoTerritoryBounds.maxX - defaultMainlandCoordinates.minX,
|
||||
height:
|
||||
defaultMainlandCoordinates.yFlipOffset -
|
||||
cayoTerritoryBounds.minY -
|
||||
defaultMainlandCoordinates.minY,
|
||||
width: mapMaximumX - defaultMainlandCoordinates.minX,
|
||||
height: mapMaximumY - defaultMainlandCoordinates.minY,
|
||||
yFlipOffset: defaultMainlandCoordinates.yFlipOffset,
|
||||
}
|
||||
|
||||
@@ -47,12 +57,7 @@ export const defaultMainlandStyle = toDefaultMapLayerStyle(
|
||||
defaultMainlandCoordinates,
|
||||
)
|
||||
export const defaultCayoStyle = toDefaultMapLayerStyle({
|
||||
minX: cayoMapCoordinates.centerX - cayoMapCoordinates.width / 2,
|
||||
minY:
|
||||
defaultMapCoordinates.yFlipOffset -
|
||||
(cayoMapCoordinates.centerY + cayoMapCoordinates.height / 2),
|
||||
width: cayoMapCoordinates.width,
|
||||
height: cayoMapCoordinates.height,
|
||||
...cayoLayerBounds,
|
||||
})
|
||||
|
||||
export const clampDefaultMapPoint = (point: MapPoint): MapPoint => ({
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
clampMapPan,
|
||||
clientPointToMapPercent,
|
||||
minimumCoverZoom,
|
||||
zoomPanAtPoint,
|
||||
} from '@/features/map/mapViewport'
|
||||
|
||||
describe('map viewport geometry', () => {
|
||||
const metrics = {
|
||||
canvasHeight: 900,
|
||||
canvasWidth: 600,
|
||||
viewportHeight: 720,
|
||||
viewportWidth: 360,
|
||||
}
|
||||
|
||||
it('raises the minimum zoom until the canvas covers the viewport', () => {
|
||||
expect(minimumCoverZoom(metrics)).toBe(0.8)
|
||||
})
|
||||
|
||||
it('clamps panning so no empty edge enters the viewport', () => {
|
||||
expect(clampMapPan({ x: 999, y: -999 }, 1, metrics)).toEqual({
|
||||
x: 120,
|
||||
y: -90,
|
||||
})
|
||||
})
|
||||
|
||||
it('converts scaled client positions into bounded map percentages', () => {
|
||||
const canvas = { height: 800, left: 100, top: 50, width: 400 }
|
||||
|
||||
expect(clientPointToMapPercent({ x: 300, y: 250 }, canvas)).toEqual({
|
||||
x: 0.5,
|
||||
y: 0.25,
|
||||
})
|
||||
expect(clientPointToMapPercent({ x: 900, y: -100 }, canvas)).toEqual({
|
||||
x: 1,
|
||||
y: 0,
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the focal map point stationary while zooming', () => {
|
||||
expect(
|
||||
zoomPanAtPoint(
|
||||
{ x: 20, y: -10 },
|
||||
1,
|
||||
2,
|
||||
{ x: 270, y: 180 },
|
||||
{ x: 360, y: 720 },
|
||||
),
|
||||
).toEqual({ x: -50, y: 160 })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,86 @@
|
||||
import type { MapPoint } from './defaultMapGeometry'
|
||||
|
||||
export type MapViewportMetrics = {
|
||||
canvasHeight: number
|
||||
canvasWidth: number
|
||||
viewportHeight: number
|
||||
viewportWidth: number
|
||||
}
|
||||
|
||||
export type MapRect = {
|
||||
height: number
|
||||
left: number
|
||||
top: number
|
||||
width: number
|
||||
}
|
||||
|
||||
const clamp = (value: number, minimum: number, maximum: number): number =>
|
||||
Math.min(maximum, Math.max(minimum, value))
|
||||
|
||||
export function minimumCoverZoom(
|
||||
metrics: MapViewportMetrics,
|
||||
minimum = 0.7,
|
||||
): number {
|
||||
if (
|
||||
metrics.canvasWidth <= 0 ||
|
||||
metrics.canvasHeight <= 0 ||
|
||||
metrics.viewportWidth <= 0 ||
|
||||
metrics.viewportHeight <= 0
|
||||
) {
|
||||
return minimum
|
||||
}
|
||||
|
||||
return Math.max(
|
||||
minimum,
|
||||
metrics.viewportWidth / metrics.canvasWidth,
|
||||
metrics.viewportHeight / metrics.canvasHeight,
|
||||
)
|
||||
}
|
||||
|
||||
export function clampMapPan(
|
||||
pan: MapPoint,
|
||||
zoom: number,
|
||||
metrics: MapViewportMetrics,
|
||||
): MapPoint {
|
||||
const maximumX = Math.max(
|
||||
0,
|
||||
(metrics.canvasWidth * zoom - metrics.viewportWidth) / 2,
|
||||
)
|
||||
const maximumY = Math.max(
|
||||
0,
|
||||
(metrics.canvasHeight * zoom - metrics.viewportHeight) / 2,
|
||||
)
|
||||
|
||||
return {
|
||||
x: clamp(pan.x, -maximumX, maximumX),
|
||||
y: clamp(pan.y, -maximumY, maximumY),
|
||||
}
|
||||
}
|
||||
|
||||
export function clientPointToMapPercent(
|
||||
point: MapPoint,
|
||||
canvas: MapRect,
|
||||
): MapPoint {
|
||||
if (canvas.width <= 0 || canvas.height <= 0) return { x: 0.5, y: 0.5 }
|
||||
|
||||
return {
|
||||
x: clamp((point.x - canvas.left) / canvas.width, 0, 1),
|
||||
y: clamp((point.y - canvas.top) / canvas.height, 0, 1),
|
||||
}
|
||||
}
|
||||
|
||||
export function zoomPanAtPoint(
|
||||
pan: MapPoint,
|
||||
currentZoom: number,
|
||||
nextZoom: number,
|
||||
focalPoint: MapPoint,
|
||||
viewportSize: MapPoint,
|
||||
): MapPoint {
|
||||
if (currentZoom <= 0 || nextZoom <= 0) return pan
|
||||
const scale = nextZoom / currentZoom
|
||||
|
||||
return {
|
||||
x: pan.x + (focalPoint.x - viewportSize.x / 2 - pan.x) * (1 - scale),
|
||||
y: pan.y + (focalPoint.y - viewportSize.y / 2 - pan.y) * (1 - scale),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user