improve snap-to-edge

This commit is contained in:
Degra02 2025-08-02 02:23:04 +02:00
parent 603329fbda
commit f3acc31a1c
2 changed files with 465 additions and 69 deletions

View file

@ -620,3 +620,56 @@ label.theme-color-label {
transform: scale(1); transform: scale(1);
} }
} }
/* Edge highlight styles for snap-to-edges */
.edge-highlight {
z-index: 999;
pointer-events: none;
animation: edge-fade-in 0.2s ease-in;
}
.original-edge-highlight {
z-index: 1000;
pointer-events: none;
animation: edge-fade-in 0.2s ease-in;
}
@keyframes edge-fade-in {
0% {
opacity: 0;
}
100% {
opacity: 0.6;
}
}
/* Right-angle snap indicators */
.right-angle-reference {
z-index: 998;
pointer-events: none;
animation: edge-fade-in 0.2s ease-in;
}
.right-angle-line {
z-index: 1001;
pointer-events: none;
animation: right-angle-pulse 2s infinite;
}
.right-angle-square {
z-index: 1002;
pointer-events: none;
animation: right-angle-pulse 2s infinite;
}
@keyframes right-angle-pulse {
0% {
opacity: 0.7;
}
50% {
opacity: 1;
}
100% {
opacity: 0.7;
}
}

View file

@ -1665,25 +1665,22 @@ editor = {
// Snap-to-edges functionality // Snap-to-edges functionality
_snap_enabled: true, _snap_enabled: true,
_snap_distance: 15, // pixels _snap_distance: 30, // pixels
_extension_area_multiplier: 4, // Extension area = snap_distance * this multiplier
_snap_to_base_map: false, _snap_to_base_map: false,
_snap_indicator: null, _snap_indicator: null,
_snap_candidates: [], _snap_candidates: [],
init_snap_to_edges: function() { init_snap_to_edges: function() {
// Initialize snap indicator layer
editor._snap_indicator = L.layerGroup().addTo(editor.map); editor._snap_indicator = L.layerGroup().addTo(editor.map);
// Override existing drawing event handlers to include snapping
editor.map.on('editable:drawing:move', editor._handle_snap_during_draw); editor.map.on('editable:drawing:move', editor._handle_snap_during_draw);
editor.map.on('editable:vertex:drag', editor._handle_snap_during_vertex_drag); editor.map.on('editable:vertex:drag', editor._handle_snap_during_vertex_drag);
// Add snap toggle to UI
editor._add_snap_controls(); editor._add_snap_controls();
}, },
_add_snap_controls: function() { _add_snap_controls: function() {
// Add snap toggle control to the map
var snapControl = L.control({position: 'topleft'}); var snapControl = L.control({position: 'topleft'});
snapControl.onAdd = function() { snapControl.onAdd = function() {
var container = L.DomUtil.create('div', 'leaflet-bar leaflet-control leaflet-control-snap'); var container = L.DomUtil.create('div', 'leaflet-bar leaflet-control leaflet-control-snap');
@ -1707,7 +1704,7 @@ editor = {
if (toggle) { if (toggle) {
toggle.classList.toggle('active', editor._snap_enabled); toggle.classList.toggle('active', editor._snap_enabled);
} }
// Clear any existing snap indicators
editor._clear_snap_indicators(); editor._clear_snap_indicators();
}, },
@ -1720,7 +1717,7 @@ editor = {
e.latlng.lng = snapped.lng; e.latlng.lng = snapped.lng;
} }
// Apply existing rounding // Apply rounding
e.latlng.lat = Math.round(e.latlng.lat * 100) / 100; e.latlng.lat = Math.round(e.latlng.lat * 100) / 100;
e.latlng.lng = Math.round(e.latlng.lng * 100) / 100; e.latlng.lng = Math.round(e.latlng.lng * 100) / 100;
}, },
@ -1734,7 +1731,6 @@ editor = {
e.latlng.lng = snapped.lng; e.latlng.lng = snapped.lng;
} }
// Apply existing rounding and other constraints
e.vertex.setLatLng([Math.round(e.latlng.lat * 100) / 100, Math.round(e.latlng.lng * 100) / 100]); e.vertex.setLatLng([Math.round(e.latlng.lat * 100) / 100, Math.round(e.latlng.lng * 100) / 100]);
}, },
@ -1744,23 +1740,42 @@ editor = {
var mapPoint = editor.map.latLngToContainerPoint(latlng); var mapPoint = editor.map.latLngToContainerPoint(latlng);
var candidates = []; var candidates = [];
// Find snap candidates from existing geometries // check for right-angle snap to current shape vertices
editor._geometries_layer.eachLayer(function(layer) { var rightAngleSnap = editor._find_right_angle_snap(latlng, mapPoint);
if (layer === editor._editing_layer) return; // Don't snap to self if (rightAngleSnap) {
candidates.push(rightAngleSnap);
}
var snapPoint = editor._find_closest_point_on_geometry(layer, latlng, mapPoint); // find snap candidates from existing geometries with area-limited infinite extension
editor._geometries_layer.eachLayer(function(layer) {
if (layer === editor._editing_layer) return; // don't snap to self
// check if layer is within the area limit for infinite extension
var allowInfiniteExtension = editor._is_layer_in_extension_area(layer, latlng, mapPoint);
var snapPoint = editor._find_closest_point_on_geometry(layer, latlng, mapPoint, allowInfiniteExtension);
if (snapPoint && snapPoint.distance < editor._snap_distance) { if (snapPoint && snapPoint.distance < editor._snap_distance) {
candidates.push(snapPoint); candidates.push(snapPoint);
} }
}); });
// Find the closest candidate // check current editing shape with infinite extension enabled
if (editor._current_editing_shape) {
var currentShapeSnap = editor._find_closest_point_on_geometry(
editor._current_editing_shape, latlng, mapPoint, true // Always enable infinite extension for current shape
);
if (currentShapeSnap && currentShapeSnap.distance < editor._snap_distance) {
candidates.push(currentShapeSnap);
}
}
// find closest candidate
if (candidates.length > 0) { if (candidates.length > 0) {
candidates.sort(function(a, b) { return a.distance - b.distance; }); candidates.sort(function(a, b) { return a.distance - b.distance; });
var best = candidates[0]; var best = candidates[0];
// Show snap indicator // show snap indicator with edge highlighting
editor._show_snap_indicator(best.latlng); editor._show_snap_indicator(best.latlng, best);
return best.latlng; return best.latlng;
} else { } else {
@ -1769,7 +1784,86 @@ editor = {
} }
}, },
_find_closest_point_on_geometry: function(layer, targetLatLng, targetMapPoint) { _is_layer_in_extension_area: function(layer, targetLatLng, targetMapPoint) {
if (!layer.getLatLngs) return false;
// skip circles entirely for infinite extension
if (layer instanceof L.Circle || layer instanceof L.CircleMarker) {
return false;
}
try {
var coordinates = [];
if (layer instanceof L.Polygon || layer instanceof L.Polyline) {
coordinates = layer.getLatLngs();
if (coordinates[0] && Array.isArray(coordinates[0])) {
coordinates = coordinates[0];
}
}
if (coordinates.length === 0) return false;
// extension area radius (in pixels)
var extensionAreaRadius = editor._snap_distance * editor._extension_area_multiplier;
// check if any vertex of the layer is within the extension area
for (var i = 0; i < coordinates.length; i++) {
var vertexMapPoint = editor.map.latLngToContainerPoint(coordinates[i]);
var distanceToVertex = vertexMapPoint.distanceTo(targetMapPoint);
if (distanceToVertex <= extensionAreaRadius) {
return true;
}
}
for (var i = 0; i < coordinates.length; i++) {
var p1 = coordinates[i];
var p2 = coordinates[(i + 1) % coordinates.length];
if (editor._edge_intersects_circle(p1, p2, targetLatLng, targetMapPoint, extensionAreaRadius)) {
return true;
}
}
return false;
} catch (error) {
return false;
}
},
_edge_intersects_circle: function(edgeStart, edgeEnd, circleCenter, circleCenterMap, radius) {
var p1Map = editor.map.latLngToContainerPoint(edgeStart);
var p2Map = editor.map.latLngToContainerPoint(edgeEnd);
// find closest point on edge to circle center
var dx = p2Map.x - p1Map.x;
var dy = p2Map.y - p1Map.y;
var length = Math.sqrt(dx * dx + dy * dy);
if (length === 0) {
return p1Map.distanceTo(circleCenterMap) <= radius;
}
var t = Math.max(0, Math.min(1,
((circleCenterMap.x - p1Map.x) * dx + (circleCenterMap.y - p1Map.y) * dy) / (length * length)
));
var closestPoint = {
x: p1Map.x + t * dx,
y: p1Map.y + t * dy
};
var distance = Math.sqrt(
Math.pow(closestPoint.x - circleCenterMap.x, 2) +
Math.pow(closestPoint.y - circleCenterMap.y, 2)
);
return distance <= radius;
},
_find_closest_point_on_geometry: function(layer, targetLatLng, targetMapPoint, allowInfiniteExtension) {
if (!layer.getLatLngs) return null; if (!layer.getLatLngs) return null;
var closestPoint = null; var closestPoint = null;
@ -1778,32 +1872,35 @@ editor = {
try { try {
var coordinates = []; var coordinates = [];
// Handle different geometry types // handle different geometry types
if (layer instanceof L.Polygon || layer instanceof L.Polyline) { if (layer instanceof L.Polygon || layer instanceof L.Polyline) {
coordinates = layer.getLatLngs(); coordinates = layer.getLatLngs();
if (coordinates[0] && Array.isArray(coordinates[0])) { if (coordinates[0] && Array.isArray(coordinates[0])) {
coordinates = coordinates[0]; // Handle polygon with holes coordinates = coordinates[0]; // Handle polygon with holes
} }
} else if (layer instanceof L.Circle || layer instanceof L.CircleMarker) { } else if (layer instanceof L.Circle || layer instanceof L.CircleMarker) {
// For circles, snap to center
var center = layer.getLatLng(); var center = layer.getLatLng();
var centerMapPoint = editor.map.latLngToContainerPoint(center); var centerMapPoint = editor.map.latLngToContainerPoint(center);
var distance = centerMapPoint.distanceTo(targetMapPoint); var distance = centerMapPoint.distanceTo(targetMapPoint);
if (distance < editor._snap_distance) { if (distance < editor._snap_distance) {
return { return {
latlng: center, latlng: center,
distance: distance distance: distance,
edgeStart: center,
edgeEnd: center,
isInfiniteExtension: false,
isRightAngle: false
}; };
} }
return null; return null;
} }
// Check each edge of the geometry // check each edge of the geometry
for (var i = 0; i < coordinates.length; i++) { for (var i = 0; i < coordinates.length; i++) {
var p1 = coordinates[i]; var p1 = coordinates[i];
var p2 = coordinates[(i + 1) % coordinates.length]; var p2 = coordinates[(i + 1) % coordinates.length];
var snapPoint = editor._find_closest_point_on_edge(p1, p2, targetLatLng, targetMapPoint); var snapPoint = editor._find_closest_point_on_edge(p1, p2, targetLatLng, targetMapPoint, allowInfiniteExtension);
if (snapPoint && snapPoint.distance < closestDistance) { if (snapPoint && snapPoint.distance < closestDistance) {
closestDistance = snapPoint.distance; closestDistance = snapPoint.distance;
closestPoint = snapPoint; closestPoint = snapPoint;
@ -1811,36 +1908,44 @@ editor = {
} }
} catch (error) { } catch (error) {
// Silently handle geometry access errors
return null; return null;
} }
return closestPoint; return closestPoint;
}, },
_find_closest_point_on_edge: function(p1, p2, targetLatLng, targetMapPoint) { _find_closest_point_on_edge: function(p1, p2, targetLatLng, targetMapPoint, allowInfiniteExtension) {
var p1Map = editor.map.latLngToContainerPoint(p1); var p1Map = editor.map.latLngToContainerPoint(p1);
var p2Map = editor.map.latLngToContainerPoint(p2); var p2Map = editor.map.latLngToContainerPoint(p2);
// Find closest point on line segment // find closest point on line (infinite or segment based on allowInfiniteExtension)
var dx = p2Map.x - p1Map.x; var dx = p2Map.x - p1Map.x;
var dy = p2Map.y - p1Map.y; var dy = p2Map.y - p1Map.y;
var length = Math.sqrt(dx * dx + dy * dy); var length = Math.sqrt(dx * dx + dy * dy);
if (length === 0) { if (length === 0) {
// Points are the same, snap to the point // points are the same, snap to the point
var distance = p1Map.distanceTo(targetMapPoint); var distance = p1Map.distanceTo(targetMapPoint);
return { return {
latlng: p1, latlng: p1,
distance: distance distance: distance,
edgeStart: p1,
edgeEnd: p2,
isInfiniteExtension: false,
isRightAngle: false
}; };
} }
// Calculate parameter t for closest point on line // calculate parameter t for closest point on line
var t = ((targetMapPoint.x - p1Map.x) * dx + (targetMapPoint.y - p1Map.y) * dy) / (length * length); var t = ((targetMapPoint.x - p1Map.x) * dx + (targetMapPoint.y - p1Map.y) * dy) / (length * length);
t = Math.max(0, Math.min(1, t)); // Clamp to line segment
// Calculate closest point // clamp t based on allowInfiniteExtension
var originalT = t;
if (!allowInfiniteExtension) {
t = Math.max(0, Math.min(1, t)); // Clamp to line segment
}
// calculate closest point
var closestMapPoint = { var closestMapPoint = {
x: p1Map.x + t * dx, x: p1Map.x + t * dx,
y: p1Map.y + t * dy y: p1Map.y + t * dy
@ -1851,18 +1956,126 @@ editor = {
Math.pow(closestMapPoint.y - targetMapPoint.y, 2) Math.pow(closestMapPoint.y - targetMapPoint.y, 2)
); );
// Convert back to lat/lng
var closestLatLng = editor.map.containerPointToLatLng(closestMapPoint); var closestLatLng = editor.map.containerPointToLatLng(closestMapPoint);
// determine if this is an infinite extension
var isInfiniteExtension = allowInfiniteExtension && (originalT < 0 || originalT > 1);
return { return {
latlng: closestLatLng, latlng: closestLatLng,
distance: distance distance: distance,
edgeStart: p1,
edgeEnd: p2,
isInfiniteExtension: isInfiniteExtension,
isRightAngle: false,
t: originalT
}; };
}, },
_show_snap_indicator: function(latlng) { _find_right_angle_snap: function(targetLatLng, targetMapPoint) {
if (!editor._current_editing_shape) return null;
try {
var coordinates = [];
if (editor._current_editing_shape.getLatLngs) {
coordinates = editor._current_editing_shape.getLatLngs();
if (coordinates[0] && Array.isArray(coordinates[0])) {
coordinates = coordinates[0]; // Handle polygon with holes
}
} else {
return null;
}
if (coordinates.length < 2) return null;
var bestRightAngleSnap = null;
var closestDistance = Infinity;
// Check each vertex for potential right-angle formation
for (var i = 0; i < coordinates.length; i++) {
var vertex = coordinates[i];
var vertexMap = editor.map.latLngToContainerPoint(vertex);
var distanceToVertex = vertexMap.distanceTo(targetMapPoint);
if (distanceToVertex > editor._snap_distance * 2) continue; // Larger radius for right-angle detection
var prevVertex = coordinates[(i - 1 + coordinates.length) % coordinates.length];
var nextVertex = coordinates[(i + 1) % coordinates.length];
var rightAngleSnap1 = editor._calculate_right_angle_snap(vertex, prevVertex, targetLatLng, targetMapPoint);
if (rightAngleSnap1 && rightAngleSnap1.distance < closestDistance) {
closestDistance = rightAngleSnap1.distance;
bestRightAngleSnap = rightAngleSnap1;
}
var rightAngleSnap2 = editor._calculate_right_angle_snap(vertex, nextVertex, targetLatLng, targetMapPoint);
if (rightAngleSnap2 && rightAngleSnap2.distance < closestDistance) {
closestDistance = rightAngleSnap2.distance;
bestRightAngleSnap = rightAngleSnap2;
}
}
return bestRightAngleSnap;
} catch (error) {
return null;
}
},
_calculate_right_angle_snap: function(vertex, adjacentVertex, targetLatLng, targetMapPoint) {
var vertexMap = editor.map.latLngToContainerPoint(vertex);
var adjacentMap = editor.map.latLngToContainerPoint(adjacentVertex);
var edgeDx = adjacentMap.x - vertexMap.x;
var edgeDy = adjacentMap.y - vertexMap.y;
var edgeLength = Math.sqrt(edgeDx * edgeDx + edgeDy * edgeDy);
if (edgeLength === 0) return null;
var perpDx = -edgeDy / edgeLength;
var perpDy = edgeDx / edgeLength;
var targetDx = targetMapPoint.x - vertexMap.x;
var targetDy = targetMapPoint.y - vertexMap.y;
var targetLength = Math.sqrt(targetDx * targetDx + targetDy * targetDy);
if (targetLength === 0) return null;
var projectionLength = targetDx * perpDx + targetDy * perpDy;
var rightAngleMapPoint = {
x: vertexMap.x + projectionLength * perpDx,
y: vertexMap.y + projectionLength * perpDy
};
var distance = Math.sqrt(
Math.pow(rightAngleMapPoint.x - targetMapPoint.x, 2) +
Math.pow(rightAngleMapPoint.y - targetMapPoint.y, 2)
);
if (distance < editor._snap_distance && Math.abs(projectionLength) > 10) { // minimum 10 pixels away from vertex
var rightAngleLatLng = editor.map.containerPointToLatLng(rightAngleMapPoint);
return {
latlng: rightAngleLatLng,
distance: distance,
edgeStart: vertex,
edgeEnd: rightAngleLatLng,
isInfiniteExtension: false,
isRightAngle: true,
rightAngleVertex: vertex,
adjacentVertex: adjacentVertex
};
}
return null;
},
_show_snap_indicator: function(latlng, snapInfo) {
editor._clear_snap_indicators(); editor._clear_snap_indicators();
// snap point indicator
var indicator = L.circleMarker(latlng, { var indicator = L.circleMarker(latlng, {
radius: 4, radius: 4,
color: '#ff6b6b', color: '#ff6b6b',
@ -1873,6 +2086,136 @@ editor = {
}); });
editor._snap_indicator.addLayer(indicator); editor._snap_indicator.addLayer(indicator);
if (snapInfo && snapInfo.edgeStart && snapInfo.edgeEnd) {
editor._show_edge_highlight(snapInfo);
}
},
_show_edge_highlight: function(snapInfo) {
if (!snapInfo.edgeStart || !snapInfo.edgeEnd) return;
// handle right-angle visualization
if (snapInfo.isRightAngle) {
editor._show_right_angle_highlight(snapInfo);
return;
}
var startPoint = snapInfo.edgeStart;
var endPoint = snapInfo.edgeEnd;
var extendedStart, extendedEnd;
if (snapInfo.isInfiniteExtension && snapInfo.t !== undefined) {
// Extend the line significantly beyond the original edge
var startMap = editor.map.latLngToContainerPoint(startPoint);
var endMap = editor.map.latLngToContainerPoint(endPoint);
var dx = endMap.x - startMap.x;
var dy = endMap.y - startMap.y;
var length = Math.sqrt(dx * dx + dy * dy);
if (length > 0) {
dx /= length;
dy /= length;
var extensionDistance = 1000;
var extStartMap = {
x: startMap.x - dx * extensionDistance,
y: startMap.y - dy * extensionDistance
};
var extEndMap = {
x: endMap.x + dx * extensionDistance,
y: endMap.y + dy * extensionDistance
};
extendedStart = editor.map.containerPointToLatLng(extStartMap);
extendedEnd = editor.map.containerPointToLatLng(extEndMap);
} else {
extendedStart = startPoint;
extendedEnd = endPoint;
}
} else {
extendedStart = startPoint;
extendedEnd = endPoint;
}
// create edge highlight line
var edgeHighlight = L.polyline([extendedStart, extendedEnd], {
color: snapInfo.isInfiniteExtension ? '#ffaa00' : '#66dd66', // Orange for infinite, green for original edge
weight: snapInfo.isInfiniteExtension ? 2 : 3,
opacity: snapInfo.isInfiniteExtension ? 0.4 : 0.6,
dashArray: snapInfo.isInfiniteExtension ? '8, 4' : null, // Dashed for infinite extension
className: 'edge-highlight'
});
editor._snap_indicator.addLayer(edgeHighlight);
// if it's an infinite extension, also show the original edge segment more prominently
if (snapInfo.isInfiniteExtension) {
var originalEdge = L.polyline([startPoint, endPoint], {
color: '#66dd66',
weight: 3,
opacity: 0.8,
className: 'original-edge-highlight'
});
editor._snap_indicator.addLayer(originalEdge);
}
},
_show_right_angle_highlight: function(snapInfo) {
var vertex = snapInfo.rightAngleVertex;
var adjacentVertex = snapInfo.adjacentVertex;
var rightAnglePoint = snapInfo.latlng;
var referenceEdge = L.polyline([vertex, adjacentVertex], {
color: '#4488ff',
weight: 2,
opacity: 0.6,
className: 'right-angle-reference'
});
editor._snap_indicator.addLayer(referenceEdge);
var rightAngleLine = L.polyline([vertex, rightAnglePoint], {
color: '#ff4488',
weight: 3,
opacity: 0.8,
dashArray: '6, 3',
className: 'right-angle-line'
});
editor._snap_indicator.addLayer(rightAngleLine);
var vertexMap = editor.map.latLngToContainerPoint(vertex);
var adjacentMap = editor.map.latLngToContainerPoint(adjacentVertex);
var rightAngleMap = editor.map.latLngToContainerPoint(rightAnglePoint);
var size = 15; // Square size in pixels
var dx1 = adjacentMap.x - vertexMap.x;
var dy1 = adjacentMap.y - vertexMap.y;
var len1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
var dx2 = rightAngleMap.x - vertexMap.x;
var dy2 = rightAngleMap.y - vertexMap.y;
var len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
if (len1 > 0 && len2 > 0) {
dx1 = (dx1 / len1) * size;
dy1 = (dy1 / len1) * size;
dx2 = (dx2 / len2) * size;
dy2 = (dy2 / len2) * size;
var corner1 = editor.map.containerPointToLatLng({x: vertexMap.x + dx1, y: vertexMap.y + dy1});
var corner2 = editor.map.containerPointToLatLng({x: vertexMap.x + dx1 + dx2, y: vertexMap.y + dy1 + dy2});
var corner3 = editor.map.containerPointToLatLng({x: vertexMap.x + dx2, y: vertexMap.y + dy2});
var rightAngleSquare = L.polyline([vertex, corner1, corner2, corner3, vertex], {
color: '#ff4488',
weight: 2,
opacity: 0.7,
fill: false,
className: 'right-angle-square'
});
editor._snap_indicator.addLayer(rightAngleSquare);
}
}, },
_clear_snap_indicators: function() { _clear_snap_indicators: function() {