diff --git a/src/uhm/lib/engine/drawingEngine.ts b/src/uhm/lib/engine/drawingEngine.ts index 482f2dc..733b8b3 100644 --- a/src/uhm/lib/engine/drawingEngine.ts +++ b/src/uhm/lib/engine/drawingEngine.ts @@ -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); diff --git a/src/uhm/lib/engine/snapUtils.ts b/src/uhm/lib/engine/snapUtils.ts new file mode 100644 index 0000000..8d58e77 --- /dev/null +++ b/src/uhm/lib/engine/snapUtils.ts @@ -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; +}