feat: implement shift/alt snapping to geometry edges in polygon drawing
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import maplibregl from "maplibre-gl";
|
||||
import { Geometry } from "@/uhm/lib/useEditorState";
|
||||
import type { ModeGetter } from "@/uhm/lib/engine/engineTypes";
|
||||
import { snapToNearestGeometry } from "@/uhm/lib/engine/snapUtils";
|
||||
|
||||
// Khởi tạo engine vẽ polygon tự do theo chuỗi click.
|
||||
export function initDrawing(
|
||||
@@ -57,7 +58,13 @@ export function initDrawing(
|
||||
function onClick(e: maplibregl.MapLayerMouseEvent) {
|
||||
if (getMode() !== "draw") return;
|
||||
|
||||
coords.push([e.lngLat.lng, e.lngLat.lat] as [number, number]);
|
||||
let lngLat = e.lngLat;
|
||||
// Dùng Shift (hoặc Alt nếu Shift bị maplibre chiếm dụng) để snap
|
||||
if (e.originalEvent.shiftKey || e.originalEvent.altKey) {
|
||||
lngLat = snapToNearestGeometry(map, e.lngLat, e.point);
|
||||
}
|
||||
|
||||
coords.push([lngLat.lng, lngLat.lat] as [number, number]);
|
||||
update(coords);
|
||||
}
|
||||
|
||||
@@ -65,9 +72,14 @@ export function initDrawing(
|
||||
function onMove(e: maplibregl.MapLayerMouseEvent) {
|
||||
if (getMode() !== "draw" || coords.length === 0) return;
|
||||
|
||||
let lngLat = e.lngLat;
|
||||
if (e.originalEvent.shiftKey || e.originalEvent.altKey) {
|
||||
lngLat = snapToNearestGeometry(map, e.lngLat, e.point);
|
||||
}
|
||||
|
||||
const preview: [number, number][] = [
|
||||
...coords,
|
||||
[e.lngLat.lng, e.lngLat.lat] as [number, number],
|
||||
[lngLat.lng, lngLat.lat] as [number, number],
|
||||
];
|
||||
update(preview);
|
||||
}
|
||||
@@ -109,11 +121,17 @@ export function initDrawing(
|
||||
}
|
||||
}
|
||||
|
||||
// Tắt tính năng box zoom và double click zoom để Shift không bị lỗi
|
||||
map.boxZoom.disable();
|
||||
map.doubleClickZoom.disable();
|
||||
|
||||
map.on("click", onClick);
|
||||
map.on("mousemove", onMove);
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
|
||||
const cleanup = () => {
|
||||
map.boxZoom.enable();
|
||||
map.doubleClickZoom.enable();
|
||||
map.off("click", onClick);
|
||||
map.off("mousemove", onMove);
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import maplibregl from "maplibre-gl";
|
||||
|
||||
const SNAP_THRESHOLD_PX = 15;
|
||||
|
||||
export function snapToNearestGeometry(
|
||||
map: maplibregl.Map,
|
||||
lngLat: maplibregl.LngLat,
|
||||
pointPx: maplibregl.Point
|
||||
): maplibregl.LngLat {
|
||||
const bbox: [maplibregl.PointLike, maplibregl.PointLike] = [
|
||||
[pointPx.x - SNAP_THRESHOLD_PX, pointPx.y - SNAP_THRESHOLD_PX],
|
||||
[pointPx.x + SNAP_THRESHOLD_PX, pointPx.y + SNAP_THRESHOLD_PX],
|
||||
];
|
||||
|
||||
const features = map.queryRenderedFeatures(bbox);
|
||||
|
||||
let nearestDist = Infinity;
|
||||
let nearestLngLat: maplibregl.LngLat | null = null;
|
||||
|
||||
const getDistSq = (p1: maplibregl.Point, p2: maplibregl.Point) => {
|
||||
return (p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2;
|
||||
};
|
||||
|
||||
// Tìm điểm gần nhất trên đoạn thẳng [a, b] so với điểm p
|
||||
const getClosestPointOnSegment = (p: maplibregl.Point, a: maplibregl.Point, b: maplibregl.Point): maplibregl.Point => {
|
||||
const atob = { x: b.x - a.x, y: b.y - a.y };
|
||||
const atop = { x: p.x - a.x, y: p.y - a.y };
|
||||
const lenSq = atob.x * atob.x + atob.y * atob.y;
|
||||
if (lenSq === 0) return new maplibregl.Point(a.x, a.y);
|
||||
|
||||
let t = (atop.x * atob.x + atop.y * atob.y) / lenSq;
|
||||
t = Math.max(0, Math.min(1, t));
|
||||
|
||||
return new maplibregl.Point(a.x + atob.x * t, a.y + atob.y * t);
|
||||
};
|
||||
|
||||
const processLineString = (line: number[][]) => {
|
||||
if (!line || line.length < 2) return;
|
||||
for (let i = 0; i < line.length - 1; i++) {
|
||||
const p1LngLat = new maplibregl.LngLat(line[i][0], line[i][1]);
|
||||
const p2LngLat = new maplibregl.LngLat(line[i + 1][0], line[i + 1][1]);
|
||||
const p1 = map.project(p1LngLat);
|
||||
const p2 = map.project(p2LngLat);
|
||||
|
||||
const closestPx = getClosestPointOnSegment(pointPx, p1, p2);
|
||||
const distSq = getDistSq(pointPx, closestPx);
|
||||
|
||||
if (distSq < nearestDist && distSq <= SNAP_THRESHOLD_PX ** 2) {
|
||||
nearestDist = distSq;
|
||||
nearestLngLat = map.unproject(closestPx);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const feature of features) {
|
||||
if (!feature.geometry) continue;
|
||||
|
||||
// Bỏ qua các layer preview hoặc edit để không tự snap vào nét đang vẽ dở.
|
||||
if (feature.layer.id.includes("preview") || feature.layer.id.includes("edit-")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const type = feature.geometry.type;
|
||||
const coords = feature.geometry.coordinates as any;
|
||||
|
||||
// Xử lý cả Polygon và LineString vì viền bản đồ (border) đôi khi được render dưới dạng LineString
|
||||
if (type === "Polygon") {
|
||||
for (const ring of coords) processLineString(ring);
|
||||
} else if (type === "MultiPolygon") {
|
||||
for (const poly of coords) {
|
||||
for (const ring of poly) processLineString(ring);
|
||||
}
|
||||
} else if (type === "LineString") {
|
||||
processLineString(coords);
|
||||
} else if (type === "MultiLineString") {
|
||||
for (const line of coords) processLineString(line);
|
||||
}
|
||||
}
|
||||
|
||||
return nearestLngLat || lngLat;
|
||||
}
|
||||
Reference in New Issue
Block a user