From 593d4179e327632f0035e59cf8d02bccdcabe91e Mon Sep 17 00:00:00 2001 From: Dennis Orlando Date: Sat, 2 Aug 2025 09:25:04 +0200 Subject: [PATCH 1/3] snap to original --- src/c3nav/editor/static/editor/js/editor.js | 43 +++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/c3nav/editor/static/editor/js/editor.js b/src/c3nav/editor/static/editor/js/editor.js index a4448da0..3919f296 100644 --- a/src/c3nav/editor/static/editor/js/editor.js +++ b/src/c3nav/editor/static/editor/js/editor.js @@ -163,6 +163,7 @@ editor = { }, _sidebar_unload: function () { // unload the sidebar. called on sidebar_get and form submit. + editor._hide_original_geometry(); editor._level_control.disable(); editor._sublevel_control.disable(); @@ -1687,6 +1688,8 @@ editor = { editor._add_snap_controls(); }, + + _add_snap_controls: function() { // add snap to edge toggle @@ -1743,6 +1746,45 @@ editor = { if (toggle) { toggle.classList.toggle('active', editor._snap_to_original_enabled); } + + // Show/hide original geometry + if (editor._snap_to_original_enabled) { + editor._show_original_geometry(); + } else { + editor._hide_original_geometry(); + } + }, + + _show_original_geometry: function() { + if (!editor._bounds_layer || editor._original_geometry_layer) return; + + // Create a copy of the original geometry with different styling + var originalFeature = editor._bounds_layer.feature; + if (!originalFeature) return; + + editor._original_geometry_layer = L.geoJSON(originalFeature, { + style: function() { + return { + stroke: true, + color: '#888888', + weight: 2, + opacity: 0.7, + fill: false, + dashArray: '5, 5', + className: 'original-geometry' + }; + }, + pointToLayer: editor._point_to_layer + }); + + editor._original_geometry_layer.addTo(editor.map); + }, + + _hide_original_geometry: function() { + if (editor._original_geometry_layer) { + editor.map.removeLayer(editor._original_geometry_layer); + editor._original_geometry_layer = null; + } }, _handle_snap_during_draw: function(e) { @@ -1785,6 +1827,7 @@ editor = { // find snap candidates from existing geometries with area-limited infinite extension editor._geometries_layer.eachLayer(function(layer) { + if (layer === editor._bounds_layer && !editor._snap_to_original_enabled) return; //don't snap to original if not toggled. if (layer === editor._editing_layer) return; // don't snap to self // check if layer is within the area limit for infinite extension From f99fcb89165c6e551f214ce74a60fca8e388cfa3 Mon Sep 17 00:00:00 2001 From: Dennis Orlando Date: Sat, 2 Aug 2025 09:35:30 +0200 Subject: [PATCH 2/3] fix styling --- src/c3nav/editor/static/editor/css/editor.scss | 16 ++++++++-------- src/c3nav/editor/static/editor/js/editor.js | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/c3nav/editor/static/editor/css/editor.scss b/src/c3nav/editor/static/editor/css/editor.scss index 8a0bbc3c..7985a1cd 100644 --- a/src/c3nav/editor/static/editor/css/editor.scss +++ b/src/c3nav/editor/static/editor/css/editor.scss @@ -567,8 +567,8 @@ label.theme-color-label { .leaflet-control-snap { background-color: white; border-radius: 4px; - border: 2px solid rgba(0,0,0,0.2); background-clip: padding-box; + /* watchout for leaflet.css trying to override a:hover with a different height/width */ a.snap-toggle, a.snap-to-original-toggle { @@ -578,19 +578,19 @@ label.theme-color-label { height: 30px; background-color: white; color: #666; - border-radius: 2px; - + border-radius: 4px; + &:hover { - background-color: #f4f4f4; - color: #333; + background-color: #a7a7a7; } - + &.active { - background-color: #45a049; + background-color: #b0ecb2; + border: 2px solid green; color: white; &:hover { - background-color: #b0ecb2; + background-color: #7ac27d; } } } diff --git a/src/c3nav/editor/static/editor/js/editor.js b/src/c3nav/editor/static/editor/js/editor.js index 3919f296..f92fa62c 100644 --- a/src/c3nav/editor/static/editor/js/editor.js +++ b/src/c3nav/editor/static/editor/js/editor.js @@ -1713,7 +1713,7 @@ editor = { var snapToOriginalControl = L.control({position: 'topleft'}); snapToOriginalControl.onAdd = function() { var container = L.DomUtil.create('div', 'leaflet-bar leaflet-control leaflet-control-snap'); - container.innerHTML = ''; L.DomEvent.on(container.querySelector('.snap-to-original-toggle'), 'click', function(e) { From 2e681dffb4d47f0b9e75128ed472bfb00d680d46 Mon Sep 17 00:00:00 2001 From: Dennis Orlando Date: Sat, 2 Aug 2025 10:58:33 +0200 Subject: [PATCH 3/3] =?UTF-8?q?toggle=20to=2090=C2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../editor/static/editor/css/editor.scss | 9 +- src/c3nav/editor/static/editor/js/editor.js | 339 +++++++++--------- 2 files changed, 175 insertions(+), 173 deletions(-) diff --git a/src/c3nav/editor/static/editor/css/editor.scss b/src/c3nav/editor/static/editor/css/editor.scss index 7985a1cd..93705fff 100644 --- a/src/c3nav/editor/static/editor/css/editor.scss +++ b/src/c3nav/editor/static/editor/css/editor.scss @@ -571,7 +571,7 @@ label.theme-color-label { /* watchout for leaflet.css trying to override a:hover with a different height/width */ - a.snap-toggle, a.snap-to-original-toggle { + a.snap-toggle, a.snap-to-original-toggle, a.snap-to-90-toggle { background-size: 30px 30px; display: block; width: 30px; @@ -595,6 +595,10 @@ label.theme-color-label { } } + a.snap-to-90-toggle { + background-color: yellow !important; + } + /* icons */ a.snap-toggle { background-image: url("/static/img/snap-to-edges-icon.svg"); @@ -602,6 +606,9 @@ label.theme-color-label { a.snap-to-original-toggle { background-image: url("/static/img/snap-to-original-icon.svg"); } + a.snap-to-90-toggle { + background-image: url("/static/img/snap-to-90-icon.svg"); + } } /* Snap indicator styles */ diff --git a/src/c3nav/editor/static/editor/js/editor.js b/src/c3nav/editor/static/editor/js/editor.js index f92fa62c..df52ecf4 100644 --- a/src/c3nav/editor/static/editor/js/editor.js +++ b/src/c3nav/editor/static/editor/js/editor.js @@ -1673,6 +1673,7 @@ editor = { // Snap-to-edges functionality _snap_enabled: false, _snap_to_original_enabled: false, + _snap_to_90_enable: false, _snap_distance: 30, // pixels _extension_area_multiplier: 4, // Extension area = snap_distance * this multiplier _snap_to_base_map: false, @@ -1687,8 +1688,7 @@ editor = { editor._add_snap_controls(); }, - - + _add_snap_controls: function() { @@ -1726,7 +1726,22 @@ editor = { }; snapToOriginalControl.addTo(editor.map); + // add snap to 90° toggle + var snapTo90Control = L.control({position: 'topleft'}); + snapTo90Control.onAdd = function() { + var container = L.DomUtil.create('div', 'leaflet-bar leaflet-control leaflet-control-snap'); + container.innerHTML = ''; + L.DomEvent.on(container.querySelector('.snap-to-90-toggle'), 'click', function(e) { + e.preventDefault(); + editor._toggle_snap_to_90(); + }); + + L.DomEvent.disableClickPropagation(container); + return container; + }; + snapTo90Control.addTo(editor.map); }, @@ -1755,6 +1770,16 @@ editor = { } }, + _toggle_snap_to_90: function() { + editor._snap_to_90_enabled = !editor._snap_to_90_enabled; + var toggle = document.querySelector('.snap-to-90-toggle'); + if (toggle) { + toggle.classList.toggle('active', editor._snap_to_90_enabled); + } + + editor._clear_snap_indicators(); + }, + _show_original_geometry: function() { if (!editor._bounds_layer || editor._original_geometry_layer) return; @@ -1819,10 +1844,12 @@ editor = { var mapPoint = editor.map.latLngToContainerPoint(latlng); var candidates = []; - // check for right-angle snap to current shape vertices - var rightAngleSnap = editor._find_right_angle_snap(latlng, mapPoint); - if (rightAngleSnap) { - candidates.push(rightAngleSnap); + // ADD THIS: check for 90° axis snap + if (editor._snap_to_90_enabled) { + var ninetyDegreeSnap = editor._find_90_degree_snap(latlng, mapPoint); + if (ninetyDegreeSnap) { + candidates.push(ninetyDegreeSnap); + } } // find snap candidates from existing geometries with area-limited infinite extension @@ -1943,6 +1970,89 @@ editor = { return distance <= radius; }, + _find_90_degree_snap: function(targetLatLng, targetMapPoint) { + if (!editor._geometries_layer) return null; + + var bestSnap = null; + var closestDistance = Infinity; + + // Check all geometry vertices for 90° alignment + editor._geometries_layer.eachLayer(function(layer) { + if (layer === editor._editing_layer) return; // don't snap to self + if (!layer.getLatLngs) return; + + 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]; + } + } else if (layer instanceof L.Circle || layer instanceof L.CircleMarker) { + coordinates = [layer.getLatLng()]; + } + + // Check each vertex for 90° alignment + for (var i = 0; i < coordinates.length; i++) { + var vertex = coordinates[i]; + var vertexMapPoint = editor.map.latLngToContainerPoint(vertex); + + // Calculate horizontal and vertical snap points + var horizontalSnap = { + x: targetMapPoint.x, + y: vertexMapPoint.y + }; + var verticalSnap = { + x: vertexMapPoint.x, + y: targetMapPoint.y + }; + + // Check horizontal alignment + var horizontalDistance = Math.abs(targetMapPoint.y - vertexMapPoint.y); + if (horizontalDistance < editor._snap_distance) { + var horizontalLatLng = editor.map.containerPointToLatLng(horizontalSnap); + var totalDistance = targetMapPoint.distanceTo(horizontalSnap); + + if (totalDistance < closestDistance && totalDistance < editor._snap_distance) { + closestDistance = totalDistance; + bestSnap = { + latlng: horizontalLatLng, + distance: totalDistance, + snapType: 'horizontal', + referenceVertex: vertex, + isRightAngle: false, + is90Degree: true + }; + } + } + + // Check vertical alignment + var verticalDistance = Math.abs(targetMapPoint.x - vertexMapPoint.x); + if (verticalDistance < editor._snap_distance) { + var verticalLatLng = editor.map.containerPointToLatLng(verticalSnap); + var totalDistance = targetMapPoint.distanceTo(verticalSnap); + + if (totalDistance < closestDistance && totalDistance < editor._snap_distance) { + closestDistance = totalDistance; + bestSnap = { + latlng: verticalLatLng, + distance: totalDistance, + snapType: 'vertical', + referenceVertex: vertex, + isRightAngle: false, + is90Degree: true + }; + } + } + } + } catch (error) { + // Skip problematic layers + } + }); + + return bestSnap; + }, + _find_closest_point_on_geometry: function(layer, targetLatLng, targetMapPoint, allowInfiniteExtension) { if (!layer.getLatLngs) return null; @@ -2052,107 +2162,7 @@ editor = { t: originalT }; }, - - _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(); @@ -2171,20 +2181,62 @@ editor = { editor._snap_indicator.addLayer(indicator); - if (snapInfo && snapInfo.edgeStart && snapInfo.edgeEnd) { + if (snapInfo && snapInfo.is90Degree) { + editor._show_90_degree_highlight(snapInfo); + } else if (snapInfo && snapInfo.edgeStart && snapInfo.edgeEnd) { editor._show_edge_highlight(snapInfo); } }, + _show_90_degree_highlight: function(snapInfo) { + var referenceVertex = snapInfo.referenceVertex; + var snapPoint = snapInfo.latlng; + + // Draw line from reference vertex to snap point + var guideLine = L.polyline([referenceVertex, snapPoint], { + color: '#00aaff', + weight: 2, + opacity: 0.8, + dashArray: '4, 4', + className: '90-degree-guide' + }); + editor._snap_indicator.addLayer(guideLine); + + // Highlight the reference vertex + var vertexHighlight = L.circle(referenceVertex, { + radius: 0.05, + color: '#00aaff', + weight: 2, + opacity: 0.8, + fillOpacity: 0.3, + className: '90-degree-vertex' + }); + editor._snap_indicator.addLayer(vertexHighlight); + + // Add axis indicator + var referenceMap = editor.map.latLngToContainerPoint(referenceVertex); + var snapMap = editor.map.latLngToContainerPoint(snapPoint); + + var axisText = snapInfo.snapType === 'horizontal' ? '─' : '│'; + var midPoint = editor.map.containerPointToLatLng({ + x: (referenceMap.x + snapMap.x) / 2, + y: (referenceMap.y + snapMap.y) / 2 + }); + + // Create a small text indicator (you might need to style this with CSS) + var textMarker = L.marker(midPoint, { + icon: L.divIcon({ + html: '
' + axisText + '
', + className: '90-degree-axis-indicator', + iconSize: [20, 20], + iconAnchor: [10, 10] + }) + }); + editor._snap_indicator.addLayer(textMarker); + }, + _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; @@ -2245,63 +2297,6 @@ editor = { 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() { if (editor._snap_indicator) { editor._snap_indicator.clearLayers();