improve snap-to-edge
This commit is contained in:
parent
603329fbda
commit
f3acc31a1c
2 changed files with 465 additions and 69 deletions
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue