feat(map): 地图右下角新增全局缩略图预览并支持点击拖动联动
This commit is contained in:
@@ -267,9 +267,12 @@ const formError = ref("");
|
|||||||
const mapEditorMode = ref<EditorMode>("select");
|
const mapEditorMode = ref<EditorMode>("select");
|
||||||
const mapStageRef = ref<HTMLElement | null>(null);
|
const mapStageRef = ref<HTMLElement | null>(null);
|
||||||
const mapCanvasRef = ref<HTMLCanvasElement | null>(null);
|
const mapCanvasRef = ref<HTMLCanvasElement | null>(null);
|
||||||
|
const mapOverviewRef = ref<HTMLCanvasElement | null>(null);
|
||||||
const suppressStageClick = ref(false);
|
const suppressStageClick = ref(false);
|
||||||
const minMapZoom = 0.6;
|
const minMapZoom = 0.6;
|
||||||
const maxMapZoom = 15;
|
const maxMapZoom = 15;
|
||||||
|
const mapOverviewWidth = 200;
|
||||||
|
const mapOverviewHeight = 130;
|
||||||
const mapZoom = ref(1);
|
const mapZoom = ref(1);
|
||||||
const mapZoomPercent = computed(() => Math.round(mapZoom.value * 100));
|
const mapZoomPercent = computed(() => Math.round(mapZoom.value * 100));
|
||||||
|
|
||||||
@@ -1853,6 +1856,123 @@ function renderMapCanvas() {
|
|||||||
context.strokeRect(left, top, width, height);
|
context.strokeRect(left, top, width, height);
|
||||||
context.restore();
|
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() {
|
function scheduleMapCanvasRender() {
|
||||||
@@ -2910,6 +3030,12 @@ watch(
|
|||||||
<button type="button" @click="zoomInMap">+</button>
|
<button type="button" @click="zoomInMap">+</button>
|
||||||
<button type="button" @click="resetMapView">重置</button>
|
<button type="button" @click="resetMapView">重置</button>
|
||||||
</div>
|
</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">
|
<div class="stage-overlay top-right">
|
||||||
{{
|
{{
|
||||||
currentConfig.mapLoaded
|
currentConfig.mapLoaded
|
||||||
@@ -3466,6 +3592,39 @@ watch(
|
|||||||
font-weight: 700;
|
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 {
|
.stage-overlay.bottom-left {
|
||||||
left: 14px;
|
left: 14px;
|
||||||
bottom: 14px;
|
bottom: 14px;
|
||||||
|
|||||||
Reference in New Issue
Block a user