@mytec: stack done, rust next
This commit is contained in:
349
docs/devlog/gpu_supp/RFCP-3.10.2-ToolMode-ClickFixes.md
Normal file
349
docs/devlog/gpu_supp/RFCP-3.10.2-ToolMode-ClickFixes.md
Normal file
@@ -0,0 +1,349 @@
|
||||
# RFCP — Iteration 3.10.2: Tool Mode System & Click Fixes
|
||||
|
||||
## Root Cause
|
||||
All click-related bugs share one root cause: multiple features compete for the same map click event. Ruler, RX point placement, site placement, and terrain profile all listen to map clicks simultaneously. There's no centralized "active tool" state that prevents conflicts.
|
||||
|
||||
## Solution: Active Tool Mode
|
||||
Create a single source of truth for which tool is currently active. Only the active tool receives map click events.
|
||||
|
||||
### Tool Modes (mutually exclusive):
|
||||
```typescript
|
||||
type ActiveTool =
|
||||
| 'none' // Default — pan/zoom only, no click actions
|
||||
| 'ruler' // Distance measurement, click to add points
|
||||
| 'rx-placement' // Link Budget RX point, single click
|
||||
| 'site-placement' // Place new site on map
|
||||
```
|
||||
|
||||
### Implementation
|
||||
|
||||
**1. Add to app store (Zustand):**
|
||||
|
||||
```typescript
|
||||
// In the main store or a new toolStore:
|
||||
interface ToolState {
|
||||
activeTool: ActiveTool;
|
||||
setActiveTool: (tool: ActiveTool) => void;
|
||||
clearTool: () => void;
|
||||
}
|
||||
|
||||
const useToolStore = create<ToolState>((set) => ({
|
||||
activeTool: 'none',
|
||||
setActiveTool: (tool) => set({ activeTool: tool }),
|
||||
clearTool: () => set({ activeTool: 'none' }),
|
||||
}));
|
||||
```
|
||||
|
||||
**2. Map click handler — single entry point:**
|
||||
|
||||
Replace all individual map click listeners with ONE handler:
|
||||
|
||||
```typescript
|
||||
// In the main Map component:
|
||||
map.on('click', (e: L.LeafletMouseEvent) => {
|
||||
const { activeTool } = useToolStore.getState();
|
||||
|
||||
switch (activeTool) {
|
||||
case 'ruler':
|
||||
handleRulerClick(e);
|
||||
break;
|
||||
case 'rx-placement':
|
||||
handleRxPlacement(e);
|
||||
break;
|
||||
case 'site-placement':
|
||||
handleSitePlacement(e);
|
||||
break;
|
||||
case 'none':
|
||||
default:
|
||||
// No action on map click — just pan/zoom
|
||||
break;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**3. Cursor changes based on active tool:**
|
||||
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
const container = map.getContainer();
|
||||
// Remove all tool cursors
|
||||
container.classList.remove('ruler-mode', 'rx-placement-mode', 'site-placement-mode');
|
||||
|
||||
switch (activeTool) {
|
||||
case 'ruler':
|
||||
container.classList.add('ruler-mode');
|
||||
break;
|
||||
case 'rx-placement':
|
||||
container.classList.add('rx-placement-mode');
|
||||
break;
|
||||
case 'site-placement':
|
||||
container.classList.add('site-placement-mode');
|
||||
break;
|
||||
default:
|
||||
// Default cursor (arrow)
|
||||
break;
|
||||
}
|
||||
}, [activeTool]);
|
||||
```
|
||||
|
||||
**4. CSS for cursors:**
|
||||
|
||||
```css
|
||||
.leaflet-container {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.leaflet-container.leaflet-dragging {
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
|
||||
.leaflet-container.ruler-mode {
|
||||
cursor: crosshair !important;
|
||||
}
|
||||
|
||||
.leaflet-container.rx-placement-mode {
|
||||
cursor: crosshair !important;
|
||||
}
|
||||
|
||||
.leaflet-container.site-placement-mode {
|
||||
cursor: cell !important;
|
||||
}
|
||||
```
|
||||
|
||||
**5. UI buttons toggle tool mode:**
|
||||
|
||||
```typescript
|
||||
// Ruler button:
|
||||
const handleRulerToggle = () => {
|
||||
if (activeTool === 'ruler') {
|
||||
clearTool(); // Toggle off
|
||||
} else {
|
||||
setActiveTool('ruler'); // Activate ruler, deactivate others
|
||||
}
|
||||
};
|
||||
|
||||
// Link Budget "Click on Map to Set RX Point" button:
|
||||
const handleRxModeToggle = () => {
|
||||
if (activeTool === 'rx-placement') {
|
||||
clearTool();
|
||||
} else {
|
||||
setActiveTool('rx-placement');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**6. Auto-deactivation:**
|
||||
- RX placement: deactivate after single click (point is set)
|
||||
- Ruler: stays active until toggled off or right-click finishes
|
||||
- Site placement: deactivate after placing site
|
||||
|
||||
---
|
||||
|
||||
## Fix: Ruler Snap to Site
|
||||
|
||||
In the ruler click handler, check proximity to existing sites:
|
||||
|
||||
```typescript
|
||||
function handleRulerClick(e: L.LeafletMouseEvent) {
|
||||
const map = e.target;
|
||||
const clickPoint = map.latLngToContainerPoint(e.latlng);
|
||||
const SNAP_THRESHOLD_PX = 20;
|
||||
|
||||
// Check all site markers
|
||||
let snappedLatLng = e.latlng;
|
||||
let snapped = false;
|
||||
|
||||
for (const site of sites) {
|
||||
const siteLatLng = L.latLng(site.lat, site.lon);
|
||||
const sitePoint = map.latLngToContainerPoint(siteLatLng);
|
||||
const pixelDist = clickPoint.distanceTo(sitePoint);
|
||||
|
||||
if (pixelDist < SNAP_THRESHOLD_PX) {
|
||||
snappedLatLng = siteLatLng;
|
||||
snapped = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add ruler point at snapped or original location
|
||||
addRulerPoint(snappedLatLng);
|
||||
|
||||
// Optional: visual feedback for snap
|
||||
if (snapped) {
|
||||
// Brief highlight on the site marker
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fix: RX Point Placement + Visual Marker
|
||||
|
||||
When in 'rx-placement' mode and map is clicked:
|
||||
|
||||
```typescript
|
||||
function handleRxPlacement(e: L.LeafletMouseEvent) {
|
||||
const { lat, lng } = e.latlng;
|
||||
|
||||
// Update Link Budget panel coordinates
|
||||
setRxCoordinates(lat, lng);
|
||||
|
||||
// Place visible marker on map
|
||||
if (rxMarkerRef.current) {
|
||||
rxMarkerRef.current.setLatLng([lat, lng]);
|
||||
} else {
|
||||
rxMarkerRef.current = L.marker([lat, lng], {
|
||||
icon: L.divIcon({
|
||||
className: 'rx-point-marker',
|
||||
html: `<div style="
|
||||
width: 14px; height: 14px;
|
||||
background: #f97316;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 6px rgba(249,115,22,0.6);
|
||||
"></div>`,
|
||||
iconSize: [14, 14],
|
||||
iconAnchor: [7, 7],
|
||||
}),
|
||||
draggable: true,
|
||||
}).addTo(map);
|
||||
|
||||
// Update coords on drag
|
||||
rxMarkerRef.current.on('drag', (ev) => {
|
||||
const pos = ev.target.getLatLng();
|
||||
setRxCoordinates(pos.lat, pos.lng);
|
||||
});
|
||||
}
|
||||
|
||||
// Draw dashed line from TX to RX
|
||||
const selectedSite = getSelectedSite();
|
||||
if (selectedSite && linkLineRef.current) {
|
||||
linkLineRef.current.setLatLngs([[selectedSite.lat, selectedSite.lon], [lat, lng]]);
|
||||
} else if (selectedSite) {
|
||||
linkLineRef.current = L.polyline(
|
||||
[[selectedSite.lat, selectedSite.lon], [lat, lng]],
|
||||
{ color: '#f97316', weight: 2, dashArray: '8,4', opacity: 0.8 }
|
||||
).addTo(map);
|
||||
}
|
||||
|
||||
// Deactivate RX placement mode (single click action)
|
||||
clearTool();
|
||||
}
|
||||
|
||||
// Cleanup when Link Budget panel closes:
|
||||
function cleanupRxMarker() {
|
||||
if (rxMarkerRef.current) {
|
||||
rxMarkerRef.current.remove();
|
||||
rxMarkerRef.current = null;
|
||||
}
|
||||
if (linkLineRef.current) {
|
||||
linkLineRef.current.remove();
|
||||
linkLineRef.current = null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fix: Terrain Profile Click-Through
|
||||
|
||||
The Terrain Profile popup and its "Terrain Profile" trigger button must stop event propagation:
|
||||
|
||||
```typescript
|
||||
// On the Terrain Profile button in the measurement overlay:
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
showTerrainProfile();
|
||||
}}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
Terrain Profile
|
||||
</button>
|
||||
|
||||
// On the Terrain Profile popup container:
|
||||
<div
|
||||
className="terrain-profile-popup"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* ... chart content ... */}
|
||||
</div>
|
||||
```
|
||||
|
||||
Also ensure the popup/panel has `pointer-events: auto` and is positioned with a high z-index above the map.
|
||||
|
||||
With the tool mode system in place, this becomes less critical since clicking terrain profile UI won't trigger ruler (ruler mode would be separate), but stopping propagation is still good practice.
|
||||
|
||||
---
|
||||
|
||||
## Fix: Default Cursor (Not Hand)
|
||||
|
||||
Override Leaflet's default grab cursor:
|
||||
|
||||
```css
|
||||
/* Global override in the app's main CSS */
|
||||
.leaflet-container {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
/* Only show grab when actually dragging */
|
||||
.leaflet-container.leaflet-dragging,
|
||||
.leaflet-container:active {
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
|
||||
/* Remove grab cursor from interactive layers too */
|
||||
.leaflet-interactive {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
/* Tool-specific cursors applied via JS class toggle */
|
||||
.leaflet-container.tool-ruler {
|
||||
cursor: crosshair !important;
|
||||
}
|
||||
|
||||
.leaflet-container.tool-rx-placement {
|
||||
cursor: crosshair !important;
|
||||
}
|
||||
|
||||
.leaflet-container.tool-site-placement {
|
||||
cursor: cell !important;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Only ONE tool can be active at a time
|
||||
- [ ] Activating Ruler deactivates RX placement and vice versa
|
||||
- [ ] Default cursor is arrow (not hand/grab)
|
||||
- [ ] Cursor changes to crosshair when Ruler is active
|
||||
- [ ] Cursor changes to crosshair when RX placement is active
|
||||
- [ ] Cursor shows grabbing only when dragging map
|
||||
- [ ] Clicking Terrain Profile button does NOT place ruler point
|
||||
- [ ] Clicking any UI panel/popup does NOT place ruler point
|
||||
- [ ] Ruler point snaps to site marker when clicking within 20px
|
||||
- [ ] RX point click places orange marker on map
|
||||
- [ ] Dashed orange line appears from TX site to RX marker
|
||||
- [ ] RX marker is draggable (updates coordinates in panel)
|
||||
- [ ] RX marker removed when Link Budget panel closes
|
||||
- [ ] Right-click finishes ruler measurement
|
||||
|
||||
## Commit Message
|
||||
|
||||
```
|
||||
fix(tools): implement active tool mode system, fix click conflicts
|
||||
|
||||
- Add ActiveTool state (none/ruler/rx-placement/site-placement)
|
||||
- Single map click handler dispatches to active tool only
|
||||
- Fix cursor: default arrow, crosshair for tools, grabbing for drag
|
||||
- Add ruler snap-to-site (20px threshold)
|
||||
- Add RX marker with draggable orange dot and dashed line
|
||||
- Stop event propagation on all UI overlays above map
|
||||
- Clean up markers when panels close
|
||||
```
|
||||
Reference in New Issue
Block a user