feat(map): 地图右下角新增全局缩略图预览并支持点击拖动联动
This commit is contained in:
@@ -267,9 +267,12 @@ const formError = ref("");
|
||||
const mapEditorMode = ref<EditorMode>("select");
|
||||
const mapStageRef = ref<HTMLElement | null>(null);
|
||||
const mapCanvasRef = ref<HTMLCanvasElement | null>(null);
|
||||
const mapOverviewRef = ref<HTMLCanvasElement | null>(null);
|
||||
const suppressStageClick = ref(false);
|
||||
const minMapZoom = 0.6;
|
||||
const maxMapZoom = 15;
|
||||
const mapOverviewWidth = 200;
|
||||
const mapOverviewHeight = 130;
|
||||
const mapZoom = ref(1);
|
||||
const mapZoomPercent = computed(() => Math.round(mapZoom.value * 100));
|
||||
|
||||
@@ -1853,6 +1856,123 @@ function renderMapCanvas() {
|
||||
context.strokeRect(left, top, width, height);
|
||||
context.restore();
|
||||
}
|
||||
|
||||
drawMapOverview();
|
||||
}
|
||||
|
||||
// 全局缩略图预览区中整张地图的实际绘制区域(contain 等比缩放)
|
||||
function getMapOverviewDrawArea() {
|
||||
const imageRatio = rosMapImage.naturalWidth && rosMapImage.naturalHeight
|
||||
? rosMapImage.naturalWidth / rosMapImage.naturalHeight
|
||||
: 178 / 50;
|
||||
const drawWidth = Math.min(mapOverviewWidth, mapOverviewHeight * imageRatio);
|
||||
const drawHeight = drawWidth / imageRatio;
|
||||
|
||||
return {
|
||||
drawWidth,
|
||||
drawHeight,
|
||||
offsetX: (mapOverviewWidth - drawWidth) / 2,
|
||||
offsetY: (mapOverviewHeight - drawHeight) / 2,
|
||||
};
|
||||
}
|
||||
|
||||
// 在右下角绘制全局缩略图,并标出当前视野在全局地图中的范围
|
||||
function drawMapOverview() {
|
||||
const canvas = mapOverviewRef.value;
|
||||
const rect = getMapStageRect();
|
||||
|
||||
if (!canvas || !rect || rect.width <= 0 || rect.height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
if (
|
||||
canvas.width !== Math.round(mapOverviewWidth * dpr) ||
|
||||
canvas.height !== Math.round(mapOverviewHeight * dpr)
|
||||
) {
|
||||
canvas.width = Math.round(mapOverviewWidth * dpr);
|
||||
canvas.height = Math.round(mapOverviewHeight * dpr);
|
||||
}
|
||||
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
context.clearRect(0, 0, mapOverviewWidth, mapOverviewHeight);
|
||||
|
||||
if (!rosMapImage.complete || !rosMapImage.naturalWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { drawWidth, drawHeight, offsetX, offsetY } = getMapOverviewDrawArea();
|
||||
|
||||
context.globalAlpha = 0.92;
|
||||
context.drawImage(rosMapImage, offsetX, offsetY, drawWidth, drawHeight);
|
||||
context.globalAlpha = 1;
|
||||
|
||||
// 当前视野对应到全局地图的比例范围
|
||||
const frame = getRosMapFrame(rect);
|
||||
const leftRatio = Math.max(0, -frame.left) / frame.width;
|
||||
const topRatio = Math.max(0, -frame.top) / frame.height;
|
||||
const rightRatio = Math.min(rect.width - frame.left, frame.width) / frame.width;
|
||||
const bottomRatio = Math.min(rect.height - frame.top, frame.height) / frame.height;
|
||||
const viewX = offsetX + leftRatio * drawWidth;
|
||||
const viewY = offsetY + topRatio * drawHeight;
|
||||
const viewWidth = (rightRatio - leftRatio) * drawWidth;
|
||||
const viewHeight = (bottomRatio - topRatio) * drawHeight;
|
||||
|
||||
context.strokeStyle = "rgba(121, 234, 255, 0.95)";
|
||||
context.lineWidth = 1.2;
|
||||
context.strokeRect(viewX, viewY, viewWidth, viewHeight);
|
||||
}
|
||||
|
||||
let mapOverviewDragging = false;
|
||||
|
||||
// 缩略图点击或拖动:将大地图视野中心切换到预览上对应的比例位置
|
||||
function jumpMapOverview(event: PointerEvent) {
|
||||
const canvas = mapOverviewRef.value;
|
||||
const rect = getMapStageRect();
|
||||
|
||||
if (!canvas || !rect || rect.width <= 0 || rect.height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const overviewRect = canvas.getBoundingClientRect();
|
||||
const { drawWidth, drawHeight, offsetX, offsetY } = getMapOverviewDrawArea();
|
||||
const ratioX = Math.min(1, Math.max(0, (event.clientX - overviewRect.left - offsetX) / drawWidth));
|
||||
const ratioY = Math.min(1, Math.max(0, (event.clientY - overviewRect.top - offsetY) / drawHeight));
|
||||
|
||||
// 平移使该比例位置位于主画布中心
|
||||
const frame = getRosMapFrame(rect);
|
||||
mapPan.x = frame.width * (0.5 - ratioX);
|
||||
mapPan.y = frame.height * (0.5 - ratioY);
|
||||
scheduleMapCanvasRender();
|
||||
}
|
||||
|
||||
function startMapOverviewDrag(event: PointerEvent) {
|
||||
mapOverviewDragging = true;
|
||||
(event.currentTarget as HTMLElement).setPointerCapture?.(event.pointerId);
|
||||
jumpMapOverview(event);
|
||||
}
|
||||
|
||||
function moveMapOverviewDrag(event: PointerEvent) {
|
||||
if (!mapOverviewDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
jumpMapOverview(event);
|
||||
}
|
||||
|
||||
function endMapOverviewDrag(event: PointerEvent) {
|
||||
mapOverviewDragging = false;
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
|
||||
if (target.hasPointerCapture?.(event.pointerId)) {
|
||||
target.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleMapCanvasRender() {
|
||||
@@ -2910,6 +3030,12 @@ watch(
|
||||
<button type="button" @click="zoomInMap">+</button>
|
||||
<button type="button" @click="resetMapView">重置</button>
|
||||
</div>
|
||||
<div class="map-overview" title="全局地图预览(点击或拖动切换视野)"
|
||||
@pointerdown="startMapOverviewDrag" @pointermove="moveMapOverviewDrag" @pointerup="endMapOverviewDrag"
|
||||
@pointercancel="endMapOverviewDrag">
|
||||
<canvas ref="mapOverviewRef" class="map-overview-canvas"></canvas>
|
||||
<div class="map-overview-label">全局视图</div>
|
||||
</div>
|
||||
<div class="stage-overlay top-right">
|
||||
{{
|
||||
currentConfig.mapLoaded
|
||||
@@ -3466,6 +3592,39 @@ watch(
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.map-overview {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
bottom: 14px;
|
||||
z-index: 6;
|
||||
padding: 6px;
|
||||
border: 1px solid rgba(86, 205, 255, 0.2);
|
||||
background: rgba(4, 18, 38, 0.78);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 16px rgba(0, 10, 24, 0.28);
|
||||
backdrop-filter: blur(2px);
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.map-overview:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.map-overview-canvas {
|
||||
display: block;
|
||||
width: 200px;
|
||||
height: 130px;
|
||||
border: 1px solid rgba(91, 208, 255, 0.14);
|
||||
background: rgba(2, 8, 18, 0.6);
|
||||
}
|
||||
|
||||
.map-overview-label {
|
||||
margin-top: 4px;
|
||||
text-align: center;
|
||||
color: rgba(184, 235, 255, 0.72);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.stage-overlay.bottom-left {
|
||||
left: 14px;
|
||||
bottom: 14px;
|
||||
|
||||
Reference in New Issue
Block a user