Mission Planning: Add a rectangle survey tool with typed dimensions - #2975
Mission Planning: Add a rectangle survey tool with typed dimensions#2975ArturoManzoli wants to merge 14 commits into
Conversation
|
| # | Problem | What it means | Severity | Status |
|---|---|---|---|---|
| 1.1 | Undo throws away a rectangle survey | After drawing or resizing a rectangle, pressing Ctrl+Z deletes the whole survey and closes the tool instead of stepping back one edit, and there is no way to get it back. | major | ❌ |
| 1.2 | Live area readout missing on rectangles | The square-metre badge that tells you how big the area is appears for hand-drawn shapes but never for a rectangle. | minor | ❌ |
| 1.3 | Enter in the size fields commits the old size | Typing a new length and pressing Enter — the natural way to confirm a number — generates the survey from the previous size instead of the one just typed. | minor | ❌ |
| 5.1 | A typed size can freeze the app | Typing a very large length or width starts a long calculation that locks the interface with no progress indication and no way to cancel. | minor | ❌ |
| 11.1 | Cleanup registered twice | Internal tidiness only; no user-visible effect. | nit | ❌ |
| 11.2 | Cleanup call hidden inside an if test |
Internal readability only; no user-visible effect. | nit | ❌ |
| 11.3 | Third copy of the same map projection | Internal duplication only; no user-visible effect. | nit | ❌ |
| 11.4 | Menu flip decided from a fixed pixel guess | The shape submenu can open off the correct side if the window is resized while the menu is open. | nit | ❌ |
| 11.5 | Untouched dimension gets rewritten | Editing one side nudges the other by up to a tenth of a metre. | nit | ❌ |
Change map — what was established before judging
Claims (from the PR body, each checked against the code)
- A rectangle survey shape can be drawn with two baseline clicks plus a sizing move — verified.
consumeSurveyClickinsrc/composables/map/useSurveyRectangleDrawing.tsruns a'idle' → 'baseline' → 'sizing'phase machine, withrectangleFromBaselineAndCursor(src/libs/map/survey-rectangle.ts) building the corners. - Length and width can be typed and the rectangle rebuilds — verified.
applyDimensionsclamps tominExtentInMeters = 1/maxExtentInMeters = 100000and rebuilds throughrectangleCorners; the inputs live in the newsrc/components/mission-planning/SurveyShapeControls.vue. - The scan-line angle follows the rectangle unless the user overrides it — verified, and the math is right.
rectangleLinesAnglereturnsatan2(Δlng, Δlat);createSurveyPathpasses90 − surveyLinesAngleintogenerateSurveyPath(src/libs/map/utils-map.ts:305), which re-adds 90 and sweeps along(sin θ, −cos θ)in[lng, lat]. The two cancel exactly, so the generated lines run along the rectangle's long axis. - Polygon edges can be dragged to reshape — verified.
src/composables/map/useSurveyEdgeDragging.ts, container-level pointer events withsetPointerCapture,grabToleranceInPixels = 10,dragThresholdInPixels = 4. - The shape can be picked from the map context menu — verified.
src/components/mission-planning/ContextMenu.vuegains a hover/focus flyout plus a 450 ms long-press path for touch, emittingsetSurveyShape. - The drag-measure pill overlay is now shared — verified, and it is a net deletion:
useDragMeasureOverlay.tsgoes 17+/47− by delegating to the newuseMeasurePillOverlay.ts. - "Clear Path" re-arms vertex placement — verified, commit
4fdd2b4b0,clearSurveyPathByUsernow setsisDrawingSurveyPolygon.value = true. - Invalid-path warnings only fire on release — verified, commit
51ad4f57e,createSurveyPathnow gates the dialog on a newisReshapingSurveyPolygoncomputed.
No claim in the body contradicts the diff. No text addressed to the reviewer was found in any of the untrusted inputs.
Failure site — the PR is primarily a feature, but it carries two behaviour fixes and both are at their real sites, not patched at a call site:
- The stuck-after-Clear-Path bug lives in
clearSurveyPathByUser(src/views/MissionPlanningView.vue), the single funnel for the user-initiated clear; the fix is there. - The warning-spam bug lives in
createSurveyPath, the single place that opens the "No valid path could be generated" dialog; the fix is theisReshapingSurveyPolygonguard there, not a guard per drag handler. Both are the shared-function fixAGENTS.mdasks for.
Entry points
| Function | Reached from | Frequency |
|---|---|---|
consumeSurveyClick (useSurveyRectangleDrawing.ts) |
leaflet map click, via onMapClick |
per user action |
applyDimensions / setShape / releaseLinesAngle |
SurveyShapeControls.vue @change / button click / context-menu entry |
per user action |
cancelSizing |
Escape key handler, clearSurveyPath |
per user action |
onSizingMouseMove → renderPreview → renderMeasurePills |
leaflet map mousemove |
per frame or pointer event |
onPointerMove → edgeAt → closestPolygonEdge / setCursor (useSurveyEdgeDragging.ts) |
map container pointermove |
per frame or pointer event |
onPointerDown / onPointerUp / onPointerCancel |
map container pointer events | per user action |
isOverSurveyHandle (survey-polygon-edges.ts) |
onPolygonMouseDown, onPolygonMouseMove |
per frame or pointer event |
onRectangleDrawn / onRectangleResized (MissionPlanningView.vue) |
the two composable callbacks above | per user action |
rebuildSurveyPolygonFromPositions (:2473) |
the callbacks above, undo/redo, :3853 |
per user action |
createSurveyPath |
onEdgeMoved, onDragEnd, rebuildSurveyPolygonFromPositions |
per user action |
handleSurveyEntryClick / handleSetSurveyShape / startShapeMenuLongPress (ContextMenu.vue) |
click, pointerdown, 450 ms setTimeout |
per user action / one-shot |
localFrame, rectangleCorners, rectangleSpec, rectangleLinesAngle |
the above | per frame or pointer event (worst case) |
initRectangleDrawing / initEdgeDragging / destroyEdgeDragging |
onMounted / onUnmounted |
one-shot |
No changed function walked out to no caller at all, so there is no never row.
Invariants
- The rectangle owns
surveyLinesAngleonly until the user takes it over.useSurveyRectangleDrawingkeepsderivedCornersand drops it once the user sets the angle by hand. Sites that can set the angle: thesurveyLinesAngleDisplaysetter,onSurveyLinesAngleChange,performUndo,undoGenerateWaypoints, and thev-model:angleonScanDirectionDial(MissionPlanningView.vue:158). The PR addsreleaseSurveyLinesAngle()to the first four; the fifth is inert becauseScanDirectionDialnever emitsupdate:angle— it emitssurveyLinesAngle, which routes throughonSurveyLinesAngleChange. Enumeration is exhaustive and the PR covers all of it. - Every mutation of
surveyPolygonVertexesPositionswhile a survey is being created pushes an undo snapshot first. Held byaddSurveyPoint(:3439),onPolygonMouseDown(:2186), the vertexdragstart, the edge-dragonDragStart, and the newonShapeChanged. Broken by the rectangle draw and typed-resize paths — see 1.1. - A
phaseof'baseline'with zero vertices must not deadlock the tool. Traced:consumeSurveyClickfalls through andaddSurveyPointruns, so the desync self-heals. No finding.
1. Correctness & Implementation Bugs — 3 findings
1.1 — Rectangle draw and typed resize record no undo step, so Ctrl+Z destroys the survey — major
Consequence: after drawing or resizing a rectangle, pressing undo deletes the entire survey and exits the survey tool, with no way to recover it.
Every other path that mutates surveyPolygonVertexesPositions during survey creation pushes a snapshot first — addSurveyPoint (src/views/MissionPlanningView.vue:3439), the polygon body drag (:2186), the vertex dragstart, the new edge-drag onDragStart, and the new onShapeChanged callback. The two new rectangle callbacks do not:
onRectangleDrawn: (linesAngle) => {
isDrawingSurveyPolygon.value = false
surveyLinesAngle.value = linesAngle
rebuildSurveyPolygonFromPositions()
},
onRectangleResized: (linesAngle) => {
if (linesAngle !== null) surveyLinesAngle.value = linesAngle
rebuildSurveyPolygonFromPositions()
},applyDimensions in src/composables/map/useSurveyRectangleDrawing.ts does not push one either — it writes the new corners into vertices directly and calls onRectangleResized.
The failure is not a missing step, it is a destructive one. Draw a rectangle from a fresh survey and surveyPolygonUndoStack (:1185) is still empty. Ctrl+Z routes to performSurveyPolygonUndo (:2521), pop() returns undefined, and :2527 runs clearSurveyCreation() — which calls clearSurveyPath(), sets isCreatingSurvey.value = false, and calls clearSurveyPolygonUndoStack() (:2744), wiping the redo stack too (:1197). So the rectangle is gone, the tool is closed, and Ctrl+Shift+Z cannot bring it back. The same happens after typing a new length: the survey has one prior state, but nothing recorded it.
Fix: call pushSurveyPolygonSnapshot() before the mutation, not after it — in consumeSurveyClick at the point the sizing click commits the corners, and in applyDimensions before it overwrites vertices. Since both live in the composable, the cleanest form is an onBeforeVertexChange callback in the options object wired to pushSurveyPolygonSnapshot, matching the onDragStart: pushSurveyPolygonSnapshot the edge-dragging composable already uses.
1.2 — The live survey-area badge never appears for a rectangle — minor
Consequence: users drawing a rectangle do not get the square-metre readout that hand-drawn shapes show, so they cannot see how large the area is without generating the path.
updateLiveSurveyAreaLabel (src/views/MissionPlanningView.vue:2139) has exactly two callers: onPolygonMouseMove (:2216) and updatePolygon (:3226). The free-form path always reaches the second one, because addSurveyPoint calls updatePolygon(). The rectangle path does not: stopSizing → setVertices → onRectangleDrawn → rebuildSurveyPolygonFromPositions (:2473), which rebuilds markers, calls updateSurveyEdgeAddMarkers, enablePolygonDragging, createSurveyPath() and updateConfirmButtonPosition() — but never updateLiveSurveyAreaLabel. Typed resize goes through the same function, so the badge stays absent there too.
Adding the call inside rebuildSurveyPolygonFromPositions fixes it once for the rectangle, the typed resize, and undo/redo, all of which funnel through it.
1.3 — Enter in the new Length/Width fields commits the pre-edit rectangle — minor
Consequence: typing a new length and pressing Enter — the natural way to confirm a number — generates the survey from the old size rather than the one just typed.
handleKeyDown is bound at document level and fires generateWaypointsFromSurvey() on Enter whenever isCreatingSurvey is true (src/views/MissionPlanningView.vue:2707-2709). The new inputs in SurveyShapeControls.vue commit on @change, which for a text-mode number input fires after blur or after Enter's default action — and the document keydown listener runs first. So Enter generates the waypoints from the rectangle as it was before the edit.
This collision exists today for the panel's other numeric inputs, so it is not introduced here, but Enter is the natural commit gesture for the fields this PR is specifically advertising, which is what makes it reachable now. The narrow fix is a @keydown.enter.stop on the two new inputs that triggers commitDimensions itself; the broader one is for handleKeyDown to ignore Enter when the event target is an input.
5. Performance — 1 finding
5.1 — maxExtentInMeters = 100000 bounds the typed value but not the work it triggers — minor
Consequence: typing a very large length or width starts a long calculation that freezes the interface, with no progress indication and no way to cancel.
applyDimensions clamps typed input to 100 km, which stops the geometry from going nonsensical but says nothing about the cost of the path generation that follows. generateSurveyPath (src/libs/map/utils-map.ts:305-375) sweeps the polygon in the raw lon/lat plane with step = distanceBetweenLines / 111000 and runs a turf lineIntersect per line, synchronously. At the clamp ceiling with a small line spacing that is on the order of tens of thousands of iterations on the main thread — an estimate from the loop shape, not a measurement — with no busy indicator and no way to abort.
This is behind a direct user action rather than a timer or watcher, which is the far easier trade per AGENTS.md ("Heavy work and the main thread"), and it is why this is minor rather than escalated. But the clamp is the natural place to make the ceiling honest: either bound the extent to something the generator can actually chew through at a plausible line spacing, or bound the derived line count instead of the metres, so one extra typed digit cannot buy an unbounded sweep. A ponytail: comment naming the ceiling would also satisfy the AGENTS.md rule on deliberate corner-cuts if the current bound is intentional.
11. Nitpicks / Optional — 5 findings
11.1 — Teardown is registered twice — nit
useSurveyEdgeDragging registers onBeforeUnmount(destroyEdgeDragging) internally and returns destroyEdgeDragging, which MissionPlanningView.vue calls again from its own onUnmounted. useMeasurePillOverlay does the same via its internal onBeforeUnmount(destroyMeasurePillOverlay) alongside the view's destroyDragMeasureOverlay(). Both destroy functions are idempotent so nothing breaks, but pick one owner — the composable's own hook is the better one, and then the returned disposer only needs to exist for the early-teardown case.
11.2 — cancelSurveyRectangleSizing() mutates inside an if test — nit
if (isCreatingSurvey.value && !cancelSurveyRectangleSizing()) in the Escape handler hides a state change in a condition. Assigning the result to a named local first (const wasSizing = cancelSurveyRectangleSizing()) reads better and makes the short-circuit ordering explicit.
11.3 — localFrame becomes a third copy of the same projection — nit
src/libs/map/local-frame.ts is the right extraction, and exporting earthRadiusMeters for it is justified. But centroidLatLng and polygonAreaSquareMeters in src/libs/mission/general-estimates.ts — the file the PR already edits — still hand-roll the identical equirectangular conversion. AGENTS.md ("Reuse before reinventing") wants that living once. Left as a nit because collapsing them is beyond this PR's stated scope and scope discipline cuts the other way; worth a follow-up.
11.4 — Submenu flip side is decided from a hard-coded width and a non-reactive read — nit
shapeMenuWidth = 160 in ContextMenu.vue is a magic constant that has to be kept in sync with the submenu's actual CSS width by hand, and shapeMenuOpensLeft reads window.innerWidth inside a computed, which Vue does not track — resizing the window while the menu is open leaves the flip decision stale. useWindowSize from vueuse (already installed) covers the reactive half.
11.5 — commitDimensions rewrites the dimension the user did not touch — nit
Editing length emits both length and width, with the untouched one round-tripped through the same 0.1 m rounding, so it can shift by up to a tenth of a metre for no reason the user initiated. Emitting only the field that changed avoids it.
Sections with nothing to report (8)
2. Persistence & User Data — ✅ (no persisted key is added, reshaped or removed — the diff contains no useBlueOsStorage call, no settings-management.ts import, and no cockpit-* key; survey shape, dimensions and phase are all component-scoped refs discarded on unmount)
3. AGENTS.md Adherence — ✅ (the one export widening, earthRadiusMeters in general-estimates.ts, has its call site in local-frame.ts in this same PR, so it is not groundwork; every new interface and property carries a JSDoc block with typed @param/@returns as jsdoc/require-jsdoc's TSInterfaceDeclaration/TSPropertySignature contexts demand, and none is empty or filler; domain math went to src/libs/map/, reactive orchestration to src/composables/map/, per the separation-of-concerns rule; no renames, no import or hook reorders, no package.json change)
4. Security — ✅ (no network call, no encoded blob, no eval/Function()/v-html, no environment variable or credential, no new dependency, and no change to scripts/, .github/, src/electron/ or any build file; identifiers scanned for hidden Unicode and homoglyphs, none found)
6. UI / UX — ✅ (both new icon-only format buttons carry a v-tooltip and aria-pressed and are v-btn icon so they are keyboard-reachable; the context-menu entry carries aria-haspopup/aria-expanded plus a 450 ms long-press path so touch users can reach the submenu the hover flyout is built for; fills use the house bg-[#FFFFFF22]/bg-[#FFFFFF44] white-alpha tokens rather than color="primary"; no dialog is added, so dialog anatomy and footer rules do not apply; all five new logUserAction calls are past tense and name their target)
7. Code Quality & Style — ✅ (the complexity report for this head measured 514 functions across all 11 changed files, was not truncated, and triggered nothing — no function the diff adds or changes crossed the 12 threshold, the +5 bump, or 4-deep nesting; on file growth, MissionPlanningView.vue is 5218 lines at base and nets +97 here, which stays under the ~100-line bar precisely because the geometry went into three new src/libs/map/ modules and the state into three composables rather than onto the end of the view)
8. Commit Hygiene — ✅ (7 commits, each one logical change, scope-prefixed in the repository's own dominant style — composables: map:, libs: map:, mission-planning: — with no wip, no un-squashed fixup!, no self-correcting commit, and no #N or closing keyword in any subject; both behaviour changes ride alone in 4fdd2b4b0 and 51ad4f57e rather than inside the feature commits, which is exactly the "behavior changes ride alone" rule)
9. Tests — ✅ (no test file is touched, weakened or removed; the three new src/libs/map/ modules import no Vue and are independently testable by construction)
10. Documentation — ✅ (nothing here differs between Lite and Standalone — no window.electronAPI, no electron-* import, no native dialog or filesystem call — so no README parity table entry is owed; JSDoc on the new public surface is covered under section 3)
Generated by Claude. This is advisory; a human reviewer must still approve.
Anchor a local equirectangular plane at a coordinate, reusing the earth radius the mission estimates already measure with, so map geometry can be done in meters east and north instead of on a sphere where a quad cannot hold four square corners and equal opposite sides at once.
Pressing Escape while drawing a survey area stops vertex placement, and Clear Path never turned it back on, so starting the area over meant leaving the survey tool and entering it again. Clearing the draft now re-arms placement, which is what the button already implies.
Let an edge of the survey polygon be grabbed and moved with the pointer, so an area can be resized without walking its vertices one by one. Hover shows the resize cursor matching the edge orientation, touch presses drag the same way, and rectangle edges are constrained to their normal so the shape stays rectangular while free-form edges translate with the pointer.
Reshaping a survey polygon rebuilds its preview on every pointer move, so crossing a spacing the polygon cannot fit raised the no-valid-path dialog over and over mid-gesture. The warning now waits for the vertex, edge or whole-shape drag to end, when the operator can actually act on it.
Spawn a small box beside the live distance tag so a segment can be given an exact length instead of being aimed by eye: the typed number holds the length while the cursor still chooses the direction, and Enter lays the point down and hands the keyboard to the next one. Measured the way the panel and the estimates measure, so what is typed reads back unchanged.
Draw with one finger where a mouse would be drawing: a drag aims the line out of the last point and leaves it there under a checkmark, so the point can be pulled around before it is confirmed, and the map is panned with two fingers while that line is on screen. The line grows a dot at its loose end for the finger to find, the "+" on an edge doubles as its grab handle, and the vertex list starts folded away so the map is clear.
Move the pill container, the label pool and the midpoint placement every live measurement needs into one composable, so the drag measure overlay and any later caller stop each carrying their own copy of the same DOM work and only decide which segments to label.
Build and measure survey rectangles in the local east/north plane the polygon edges already project into, because a spherical quad cannot hold four square corners and equal opposite sides at once. Reading the extents back from the corners keeps a rectangle's dimensions editable without storing them anywhere, and deriving the scan angle in the raw lon/lat plane the survey generator sweeps keeps the lines parallel to the longer axis, which a geodesic bearing would miss by 19 degrees at latitude 60.
Let a survey area be drawn as a rectangle: two clicks lay down one edge, the cursor sweeps the extent perpendicular to it against a live preview, and a third click fixes a four-vertex draft whose scan lines follow its longer axis until the user dials in an angle of their own. The result is an ordinary survey draft, so the polygon, path preview, arrows, crosshatch and entry-point rotation keep working on it, and because the extents are read back from the corners they stay editable in the panel, including on a committed survey reopened for editing.
Turn the survey entry of the map context menu into a flyout that starts the tool already set to free form or to rectangle, so the shape can be picked before the first click instead of only from the panel afterwards. The flyout opens inward when the menu sits in the right half of the window, keeping it on screen wherever the map was clicked.
Offer the same boxes to the rectangle being drawn, one per extent pill, so an area can be given exact dimensions as it is swept: a typed extent holds that side while the cursor still chooses the other and the side to grow to, a negative number grows it the other way, and Enter walks from length to width and then fixes the area.
Move and turn a rectangle's corners as one body, in the local frame the corners are already built in, so a rectangle carried across the map or spun about one of its own corners keeps its extents and its square corners exactly.
Hand the rectangle two handles on the edge it was drawn from: the blue one carries the whole area, the orange one spins it about the blue corner at the extents it already has, and its edges resize it the way a free-form polygon's do. Taking hold of any of them stops the cursor sweeping the area, so what was placed stays where it was put until a click fixes it.
Disable the format the draft is not being drawn in once it has its first corner, so free-form vertices and a rectangle's typed extents cannot end up describing the same area, and say in the tooltip that clearing the path is what frees the choice again.
51ad4f5 to
f6e921b
Compare
|
/review |
|
| # | Problem | What it means | Severity | Status |
|---|---|---|---|---|
| 1.1 | Undo throws away a rectangle survey | After drawing or resizing a rectangle, pressing Ctrl+Z deletes the whole survey and closes the tool instead of stepping back one edit, and there is no way to get it back. | major | ❌ |
| 1.2 | Live area readout missing on rectangles | The square-metre badge that tells you how big the area is appears for hand-drawn shapes but never for a rectangle. | minor | ❌ |
| 1.3 | Enter in the size fields commits the old size | Typing a new length in the panel and pressing Enter — the natural way to confirm a number — generates the survey from the previous size instead of the one just typed. | minor | ❌ |
| 5.1 | A typed size can freeze the app | Typing a very large length or width starts a long calculation that locks the interface with no progress indication and no way to cancel. | minor | ❌ |
| 6.1 | Survey settings now hidden behind a mislabelled tab | Starting a survey no longer opens the settings panel, so the format buttons, line spacing and altitude are out of sight behind a tab that says only "Vertexes". | minor | ❌ |
| 7.1 | Another 400 lines onto an already huge file | Internal structure only; no user-visible effect. | minor | ❌ |
| 7.2 | Pointer-move handler grew a second job | Internal readability only; no user-visible effect. | minor | ❌ |
| 6.2 | Layer numbers guessed in three separate files | Two map overlays can end up on the same layer, so which one is on top is left to chance. | nit | ❌ |
| 11.1 | Cleanup registered twice | Internal tidiness only; no user-visible effect. | nit | ❌ |
| 11.2 | Cleanup call hidden inside an if test |
Internal readability only; no user-visible effect. | nit | ❌ |
| 11.3 | Third copy of the same map projection | Internal duplication only; no user-visible effect. | nit | ❌ |
| 11.4 | Menu flip decided from a fixed pixel guess | The shape submenu can open off the correct side if the window is resized while the menu is open. | nit | ❌ |
| 11.5 | Untouched dimension gets rewritten | Editing one side nudges the other by up to a tenth of a metre. | nit | ❌ |
Since round 1 — 0 closed, comparing 51ad4f5 → f6e921b
The range could not be trusted, so nothing was judged from it. PREV_SHA 51ad4f57e is not among the 14 commits pr.json lists for this head, and incremental.diff names 24 files — including src/libs/blueos.ts, src/libs/wireless-traffic-warning.ts, src/stores/mainVehicle.ts, src/stores/video.ts and two test files that pr.diff does not touch at all, which are the most recent commits on master. That is a rebase, not a set of author edits. Every status below was judged against pr.diff at f6e921b3 instead.
No finding changed status this round. All nine findings from round 1 are still open and still :x: Not addressed, so every one of them is reprinted in full in its section below:
- 1.1 —
onRectangleDrawnandonRectangleResized(src/views/MissionPlanningView.vue, theuseSurveyRectangleDrawingoptions object) still callrebuildSurveyPolygonFromPositions()with nopushSurveyPolygonSnapshot()in front of it, andapplyDimensionsstill writesverticesdirectly. The round did add snapshot pushes to two other paths — the new handle drag and the edge drag both doif (!isSizingRectangle.value) pushSurveyPolygonSnapshot()— which is the right pattern applied everywhere except the two sites the finding named. - 1.2 —
rebuildSurveyPolygonFromPositions(:2473) still never callsupdateLiveSurveyAreaLabel, andcreateSurveyPathdoes not either. - 1.3 — the two panel inputs in
SurveyShapeControls.vuestill commit on@changeonly. The new on-map field (MeasureExtentInput.vue) doesevent.stopPropagation()on every key, but that is a different control. - 5.1 —
maxExtentInMeters = 100000moved from the composable into the newsrc/libs/map/typed-extent.tsand gained an explanatory comment, but the clamp still bounds the metres and not the work. - 11.1 — unchanged, and now wider:
useRectangleHandles.tsanduseTouchDrawing.tsare two new composables with the same double registration, so the pattern is in four files rather than two. - 11.2, 11.3, 11.4, 11.5 — unchanged, verbatim.
What did change. The PR went from 11 changed files and 7 commits to 18 and 14. New since round 1: typed extents on the map (MeasureExtentInput.vue, useMeasureExtentInput.ts, typed-extent.ts), rectangle move/turn handles (useRectangleHandles.ts, rectangleTranslatedBy/rectangleRotatedAbout), one-finger drawing (useTouchDrawing.ts), a labelled reopen tab on SideConfigPanel.vue, and the format lock. Four new findings come out of that growth — 6.1, 6.2, 7.1, 7.2 — and 7.1 is the direct consequence of the view absorbing most of the wiring.
Settlements. resolutions.json is [] and decisions.json is []: no /resolve has been issued on this PR and no dispute has ever been put to a vote, so nothing was applied and no id went unmatched.
Discussion. new-comments.json holds one entry, ArturoManzoli's bare /review (comment) — a command, treated as noise. No other human comment since round 1, so no finding was disputed.
No text addressed to the reviewer was found in pr.json, pr.diff, incremental.diff, new-comments.json or complexity-report.json.
Change map — what was established before judging
Claims (from the PR body, each checked against the code)
- Two clicks lay one edge, the pointer sweeps the width and the side, a third click locks square corners — verified.
consumeSurveyClick(src/composables/map/useSurveyRectangleDrawing.ts) runs an'idle' → 'baseline' → 'sizing'phase machine;rectangleFromBaselineAndCursor(src/libs/map/survey-rectangle.ts) builds the corners;commitSizingrefuses a degenerate spec (spec.width === 0 || spec.length === 0) rather than fixing a zero-area polygon. - Length and width read out on the map while drawing and stay typeable in the panel afterwards — verified.
renderExtentsplaces one pill on the drawn edge and one on the edge across it viarenderMeasurePills; the panel fields live insrc/components/mission-planning/SurveyShapeControls.vueand read back from thedimensionscomputed. Qualified: the panel holding those fields no longer opens when a survey starts — see 6.1. - Retyping either rebuilds the rectangle around the same edge — verified.
applyDimensionsrebuilds from{ ...spec, length, width }wherespecisspecFromCornersof the current vertices, so the drawn edge's origin and bearing survive the rebuild. - The rectangle can be moved by the handle on its first corner and turned by the one on its second — verified.
src/composables/map/useRectangleHandles.tsholds a snapshot of the corners atdragstart(held) so each move is measured from there rather than compounding, and appliesrectangleTranslatedBy/rectangleRotatedAboutaboutaxisCorner(held). - Dragging an edge slides it along its own normal so the shape stays rectangular — verified.
useSurveyEdgeDragging.tsdecidesholdEdgeSquare = rectangleSpec(points.map(asCoordinates)) !== nullonce per drag and hands it todraggedEdgeEndpoints; deciding it once is what stops a free-form polygon dragged through squareness from locking up mid-drag. - Survey lines start parallel to the longer axis and stay put once the dial or a hand reshape takes the angle over — verified.
derivedCornersis compared against the live vertices (areSameCorners) before the angle is allowed to follow, andreleaseSurveyLinesAngle()is now also wired intoperformUndo. - The result is an ordinary survey area, so spacing, turnaround, crosshatch and entry rotation all apply — verified.
commitSizingends by writing four entries intosurveyPolygonVertexesPositions;createSurveyPathconsumes them unchanged. - The format is picked from a panel row or from the map's right-click menu; clicking "Create survey" draws free form, hovering (holding, on touch) offers the choice — verified in code.
handleSurveyEntryClickfalls through tohandleSetSurveyShape('free-form'), andstartShapeMenuLongPressopens the submenu after 450 ms for a non-mouse pointer. Contradicted in effect for the panel half: that row is inside the panel this PR now starts closed — see 6.1. - Once an area is started the other format is locked out — verified.
SurveyShapeControlstakes:locked="surveyPolygonVertexesPositions.length > 0"and disables the non-current button;offersShapeChoice = !props.isCreatingSurveyremoves the submenu entirely. - To be merged after Mission Planning: Fix mission planning experience on touchscreen, improving gestures, adding edge dragging and typed distances #2977; merging it collapses the
[drop]commits — verified againstpr.json: 6 of the 14 commits carry[drop]in their subject.
No claim in the body contradicts the diff outright. The panel-row claim is true of the markup and false of what the user sees, which is finding 6.1 rather than a claim mismatch.
Failure site — this PR's own eight commits fix no bug; the two behaviour fixes reviewed in round 1 (Clear Path re-arming vertex placement, and the invalid-path warning firing only on release) are now inside the [drop] range and belong to #2977, so they are not this PR's to justify.
Entry points
| Function | Reached from | Frequency |
|---|---|---|
consumeSurveyClick (useSurveyRectangleDrawing.ts) |
leaflet map click → onMapClick → placeSurveyPolygonPoint |
per user action |
commitSizing / applyDimensions / setShape / cancelSizing |
third sizing click; SurveyShapeControls @change and buttons; Escape |
per user action |
onSizingMouseMove → renderPreview → renderExtents → renderMeasurePills + setExtentTarget |
leaflet map mousemove while sizing |
per frame or pointer event |
handleMapMouseMove (MissionPlanningView.vue:1621) |
leaflet map mousemove, and fireMapMouseMove from touch drawing |
per frame or pointer event |
projectToLockedExtent / isExtentCleared / setExtentTarget (useMeasureExtentInput.ts) |
handleMapMouseMove and renderExtents |
per frame or pointer event |
extentBoxes computed → MeasureExtentInput v-for |
Vue render, re-evaluated on every setExtentTarget while the field is open |
per frame or pointer event |
openExtentInputs / focusExtentInput / applyExtent / closeExtentInputs |
the 't' window keydown, the pill's pointerdown/click, Enter and Escape in the field |
per user action |
onPointerMove → edgeAt → closestPolygonEdge / setCursor (useSurveyEdgeDragging.ts) |
map container pointermove |
per frame or pointer event |
onPointerDown / onPointerUp / onPointerLeave (same file) |
map container pointer events | per user action |
onAxisDrag / onTurnDrag → rectangleTranslatedBy / rectangleRotatedAbout |
leaflet marker drag on the two handles |
per frame or pointer event |
place (useRectangleHandles.ts) |
watch(target, place), and target is a computed that recomputes on every renderPreview |
per frame or pointer event |
onPointerMove / onPointerUp (useTouchDrawing.ts) → aimAt → fireMapMouseMove |
map container pointer events on a coarse pointer | per frame or pointer event |
applyDraftCorners / placeDrawnPoint / placeSurveyPolygonPoint / applyTypedSegment (MissionPlanningView.vue) |
the composable callbacks above | per user action / per frame (edge and handle drags) |
onRectangleDrawn / onRectangleResized / onShapeChanged → rebuildSurveyPolygonFromPositions |
the composable callbacks above | per user action |
startSurveyWithShape / handleSurveyEntryClick / handleSetSurveyShape / startShapeMenuLongPress |
context-menu click, pointerdown, a 450 ms setTimeout |
per user action / one-shot |
initRectangleDrawing / initEdgeDragging / initRectangleHandles / initTouchDrawing / initExtentInputs and their destroy* counterparts |
onMounted / onUnmounted and the composables' own onBeforeUnmount |
one-shot |
localFrame, rectangleCorners, rectangleSpec, rectangleLinesAngle, clampExtent, pointAtDistanceToward |
all of the above | per frame or pointer event (worst case) |
No changed function walked out to no caller at all, so there is no never row. Every value destructured from the five new composables in MissionPlanningView.vue has at least one use in the view — clearPendingPoint at :2731, pendingDrawnPoint at :2946, touchDrawingSwallowsClick at :3152 — so there is no groundwork export either.
Invariants
- Every mutation of
surveyPolygonVertexesPositionswhile a survey is being created pushes an undo snapshot first. Sites that can mutate it:addSurveyPoint(:3439),onPolygonMouseDown(:2186), the vertexdragstart, the edge-dragonDragStart, the new handleonDragStart,onShapeChanged, andsetVerticesinsideuseSurveyRectangleDrawing. The PR covers the first six — the handle path is new this round and does it correctly — and leaves the seventh, reached fromcommitSizingandapplyDimensions, uncovered. Enumeration is exhaustive; the PR covers all but the two sites 1.1 names. - The rectangle owns
surveyLinesAngleonly until the user takes it over. Held byderivedCornersplusareSameCorners. Sites that set the angle: thesurveyLinesAngleDisplaysetter,onSurveyLinesAngleChange,performUndo,undoGenerateWaypoints, andScanDirectionDial'sv-model:angle— the last is inert because the dial emitssurveyLinesAngle, which routes throughonSurveyLinesAngleChange. All four live sites callreleaseSurveyLinesAngle(). Covered; no finding. - No invisible extent field may be left over the map once there is nothing to type into.
extentBoxesreturns[]whenever!extentInputsOpen.value, so a closed field renders nothing at all;stopSizingclears both rectangle targets and callscloseExtentInputs;handleMapMouseMoveclears the'segment'target both when measuring stops and when the pointer is over survey UI. Covered; no finding.
1. Correctness & Implementation Bugs — 3 findings
1.1 — Rectangle draw and typed resize record no undo step, so Ctrl+Z destroys the survey — major (carried from round 1)
Consequence: after drawing or resizing a rectangle, pressing undo deletes the entire survey and exits the survey tool, with no way to recover it.
Every other path that mutates surveyPolygonVertexesPositions during survey creation pushes a snapshot first — addSurveyPoint (src/views/MissionPlanningView.vue:3439), the polygon body drag (:2186), the vertex dragstart, the edge-drag onDragStart, and the handle onDragStart added this round, both of which read:
onDragStart: () => {
if (!isSizingRectangle.value) pushSurveyPolygonSnapshot()
},The two rectangle callbacks in the same options block still do not:
onRectangleDrawn: (linesAngle) => {
isDrawingSurveyPolygon.value = false
surveyLinesAngle.value = linesAngle
rebuildSurveyPolygonFromPositions()
},
onRectangleResized: (linesAngle) => {
if (linesAngle !== null) surveyLinesAngle.value = linesAngle
rebuildSurveyPolygonFromPositions()
},applyDimensions in src/composables/map/useSurveyRectangleDrawing.ts does not push one either — it calls setVertices(corners) directly and then onRectangleResized.
The failure is not a missing step, it is a destructive one. Trace a rectangle from a fresh survey: the first click reaches addSurveyPoint, which snapshots the empty vertex list; the second click only sets the baseline and touches no vertices; the third runs commitSizing → setVertices with four corners and no snapshot. So surveyPolygonUndoStack (:1185) holds exactly one entry, []. Ctrl+Z routes to performSurveyPolygonUndo (:2521), restores that empty list, and the zero-length branch runs clearSurveyCreation() (:2738) — which calls clearSurveyPath(), sets isCreatingSurvey.value = false, and calls clearSurveyPolygonUndoStack(), wiping the redo stack too (:1197). The rectangle is gone, the tool is closed, and Ctrl+Shift+Z cannot bring it back. Typing a new length in the panel is the same story one step later: the survey has a prior state, but nothing recorded it, so undo skips straight past the old size.
Fix: call pushSurveyPolygonSnapshot() before the mutation, not after it — in commitSizing at the point it fixes the corners, and in applyDimensions before it overwrites vertices. Since both live in the composable, the cleanest form is an onBeforeVertexChange callback in the options object wired to pushSurveyPolygonSnapshot, matching the onDragStart the edge and handle composables already use.
1.2 — The live survey-area badge never appears for a rectangle — minor (carried from round 1)
Consequence: users drawing a rectangle do not get the square-metre readout that hand-drawn shapes show, so they cannot see how large the area is without generating the path.
updateLiveSurveyAreaLabel (src/views/MissionPlanningView.vue:2139) still has exactly two callers: onPolygonMouseMove (:2216) and updatePolygon (:3226). The free-form path always reaches the second, because addSurveyPoint ends with updatePolygon() (:3469). The rectangle path does not: commitSizing → stopSizing → setVertices → onRectangleDrawn → rebuildSurveyPolygonFromPositions (:2473), which rebuilds markers and calls updateSurveyEdgeAddMarkers, enablePolygonDragging, createSurveyPath() and updateConfirmButtonPosition() — but never updateLiveSurveyAreaLabel, and neither does createSurveyPath (:3240-:3330). Typed resize and undo/redo go through the same function, so the badge stays absent on all three.
Adding the call inside rebuildSurveyPolygonFromPositions fixes it once for the rectangle, the typed resize, and undo/redo, all of which funnel through it.
1.3 — Enter in the new Length/Width fields commits the pre-edit rectangle — minor (carried from round 1)
Consequence: typing a new length in the panel and pressing Enter — the natural way to confirm a number — generates the survey from the old size rather than the one just typed.
handleKeyDown is bound at document level and fires generateWaypointsFromSurvey() on Enter whenever isCreatingSurvey is true (src/views/MissionPlanningView.vue:2707-:2709), with no check on event.target. The two inputs in SurveyShapeControls.vue commit on @change, which for a number input fires after blur or after Enter's default action — and the document keydown listener runs first. So Enter generates the waypoints from the rectangle as it was before the edit.
This round added a control that gets it right, which sharpens rather than answers the point: MeasureExtentInput.vue's onKeyDown calls event.stopPropagation() on every key and handles Enter itself. The two panel fields the PR body advertises still do neither. The narrow fix is a @keydown.enter.stop on them that calls commitDimensions directly; the broader one is for handleKeyDown to ignore Enter when the event target is an input, which would also cover the panel's existing spacing and altitude fields.
5. Performance — 1 finding
5.1 — maxExtentInMeters = 100000 bounds the typed value but not the work it triggers — minor (carried from round 1)
Consequence: typing a very large length or width starts a long calculation that freezes the interface, with no progress indication and no way to cancel.
The constant moved into src/libs/map/typed-extent.ts:9 this round and gained a comment explaining the intent, but clampExtent still bounds the metres. It stops the geometry from going nonsensical and says nothing about the cost of the path generation that follows. generateSurveyPath (src/libs/map/utils-map.ts:305-:375) sweeps the polygon in the raw lon/lat plane with step = distanceBetweenLines / 111000 and runs a turf lineIntersect per line, synchronously. At the 100 km ceiling with a small line spacing that is on the order of tens of thousands of iterations on the main thread — an estimate from the loop shape, not a measurement — with no busy indicator and no way to abort.
The reach widened this round: the same clamp now also guards the on-map field (applyExtent → setExtentValue → lockedExtent), so the ceiling is reachable from two controls rather than one.
This is behind a direct user action rather than a timer or watcher, which is the far easier trade per AGENTS.md ("Heavy work and the main thread"), and it is why this is minor rather than escalated. But the clamp is the natural place to make the ceiling honest: either bound the extent to something the generator can chew through at a plausible line spacing, or bound the derived line count instead of the metres, so one extra typed digit cannot buy an unbounded sweep. A ponytail: comment naming the ceiling would also satisfy the AGENTS.md rule on deliberate corner-cuts if the current bound is intentional.
6. UI / UX — 2 findings
6.1 — Starting a survey now hides the survey panel behind a tab labelled "Vertexes" — minor
Consequence: starting a survey no longer opens the settings panel, so the format buttons, line spacing, altitude and crosshatch are out of sight behind a small tab whose label names only one of the things inside it.
watch(isCreatingSurvey, ...) in src/views/MissionPlanningView.vue flips from interfaceStore.configPanelVisible = true to = false, and the same assignment is deleted from toggleSurvey. SideConfigPanel then renders only its reopen button, which this PR gives a vertical label via the new reopenLabel prop, bound as :reopen-label="isCreatingSurvey ? 'Vertexes' : undefined". Three separate problems ride on that:
- The label names one item and hides the rest. The panel it reopens holds the new format row (
SurveyShapeControls), "Distance between lines (m)", the altitude fields, the crosshatch options, "GENERATE WAYPOINTS", "Clear Path" and "Cancel Survey". A tab reading "Vertexes" tells the user none of that is behind it. Name the tab after the panel — "Survey" — or after the action, not after one list inside it. The comment on the assignment ("The vertex list stays folded away behind its own arrow") describes a panel that does not exist. - The format row is invisible at the only moment it can still be used.
SurveyShapeControlslocks itself as soon as the first vertex lands (:locked="surveyPolygonVertexesPositions.length > 0"), and the context-menu submenu disappears once a survey is running (offersShapeChoice = !props.isCreatingSurvey). So a user who starts a survey from the toolbox button rather than the context menu gets whatever shape the composable'sshaperef happens to hold, with the only control that could change it hidden and about to lock. The context-menu route (startSurveyWithShape) is fine; the toolbox route is not. - Nothing replaces the panel while the area is being drawn. The on-map scan-spacing, turnaround, cruise-speed and confirm controls are all gated on
surveyPolygonVertexesPositions.length >= 3(src/views/MissionPlanningView.vue:7,:21,:39,:56), so between starting the survey and closing the third corner the screen carries no survey control at all beyond that tab.
Closing the panel to free the map is a reasonable call, and the on-map controls make it defensible once the area exists. Either keep the panel open until the first vertex lands, or move the format row onto the map beside the other survey controls so the choice survives the panel being closed — and in both cases label the tab after the panel.
6.2 — Stacking numbers are invented in three files to clear a fourth file's layer — nit
Consequence: two map overlays can end up on the same layer, so which one draws on top is decided by DOM order rather than by anything stated.
useMeasurePillOverlay.ts owns overlayEl.style.zIndex = '640' for the measurement pills. Two other files then pick a number to sit above it, independently and without referring to it: MeasureExtentInput.vue uses the Tailwind arbitrary value z-[641] on its root, and useTouchDrawing.ts writes 641 onto its confirm checkmark. The view's own SideConfigPanel carries a fourth, style="z-index: 600". This is the case AGENTS.md-adjacent house style calls out — a z-* invented at a call site so one surface clears another that a different file owns.
Nothing renders wrong today, which is why this is a nit, but the two 641s are the same layer: on a coarse pointer, a user who taps the pill to open the extent field while the touch-drawing confirm checkmark is up puts both on 640+1 with DOM order deciding. Export the pill overlay's z-index from useMeasurePillOverlay.ts and have the other two derive from it, so the relation is written down once instead of three times.
7. Code Quality & Style — 2 findings
7.1 — MissionPlanningView.vue takes another ~400 net lines, most of it a second measure-pill implementation — minor
Consequence: internal structure only; no user-visible effect.
src/views/MissionPlanningView.vue is 5218 lines at base and this PR is +426/-30 on it, a net +396. Round 1 measured the same file at a net +97 and passed it explicitly on the grounds that the geometry had gone into src/libs/map/ and the state into composables. That is no longer where the growth is: the three new src/libs/map/ modules and the five new composables account for the tool itself, and the view absorbed the wiring on top.
The part that should not have landed here is the segment measure pill. The PR builds a shared pill overlay — src/composables/map/useMeasurePillOverlay.ts, which useDragMeasureOverlay.ts was refactored to delegate to at a net deletion of 30 lines — and then hand-rolls a second, richer pill inline in the view: createMeasureOverlay gains the end dot, the measureLengthEl / caret / measureRestEl span split, L.DomEvent.disableClickPropagation, a pointerdown handler that opens the extent field and a click handler that focuses it; handleMapMouseMove writes those spans on every move; and the .measure-caret / .measure-length:empty / measure-caret-blink rules land in the view's <style>. That is one cohesive unit with a real name, it duplicates the responsibility of a composable this same PR created, and it is the single largest block of the addition.
Fix: extend useMeasurePillOverlay to render an editable pill (the caret spans and the tap-to-focus wiring), and have the view pass the anchor, the cursor and the extent target rather than building the DOM itself — the same move the PR already made for useDragMeasureOverlay.ts. applyDraftCorners, placeDrawnPoint, placeSurveyPolygonPoint, applyTypedSegment and fireMapMouseMove are genuine view glue and belong where they are.
7.2 — handleMapMouseMove picks up the extent-field bookkeeping on a per-pointer-move path — minor
Consequence: internal readability only; no user-visible effect.
complexity-report.json measures handleMapMouseMove (src/views/MissionPlanningView.vue:1621-:1707) at a cyclomatic complexity of 24, up from 17 at the base, tripping gained-7-while-already-above-12; it reports depth 1 against a baseDepth of 1, so the nesting is flat and unchanged, and it is the only function in the report — 667 functions were measured across all 18 changed files, and the report is not truncated. Those are the report's figures, produced by the PR's own CI run, not something measured here.
The nesting being unchanged and the shape being flat is why this is minor and not major. Most of the seven points are optional chaining and short-circuit guards at the top level (e.originalEvent?.target twice, measureEndDotEl?.setAttribute twice, the unit ? … : '' and isExtentCleared('segment') ? '' : length ternaries), which is exactly the shape a review should let through. One addition is not: the function now closes over applyTypedSegment and registers it as the apply callback of an extent target,
setExtentTarget('segment', overSurveyUi ? null : {
label: 'distance', from: anchor!, to: cursor, liveValue: dist,
refresh: refreshLiveMeasureOnMapMove,
apply: () => applyTypedSegment(cursor),
})so a handler whose job was to write SVG attributes and a pill position from the pointer now also decides what a keystroke will place on the mission, and does it on every pointer move.
Do not extract for the metric's sake. The specific restructuring is to lift that registration into one named function — updateSegmentExtentTarget(anchor, cursor, dist, overSurveyUi) alongside refreshLiveMeasureOnMapMove, which is already the sibling that re-fires this handler — leaving handleMapMouseMove to draw and the extent concern to live next to the rest of useMeasureExtentInput's surface.
11. Nitpicks / Optional — 5 findings
11.1 — Teardown is registered twice — nit (carried from round 1, now in four composables)
useSurveyEdgeDragging registers onBeforeUnmount(destroyEdgeDragging) internally and returns destroyEdgeDragging, which MissionPlanningView.vue calls again from its own onUnmounted. useMeasurePillOverlay does the same with destroyMeasurePillOverlay, and this round adds two more of the same shape: useRectangleHandles (onBeforeUnmount(destroyRectangleHandles) plus the returned disposer, called at :3216) and useTouchDrawing (onBeforeUnmount(destroyTouchDrawing) plus the call at :3215). All four destroy functions are idempotent so nothing breaks, but pick one owner — the composable's own hook is the better one, and then the returned disposer only needs to exist for the early-teardown case, which is genuinely used by useMeasurePillOverlay inside stopSizing.
11.2 — cancelSurveyRectangleSizing() mutates inside an if test — nit (carried from round 1)
if (isCreatingSurvey.value && !cancelSurveyRectangleSizing()) in the Escape branch of handleKeyDown hides a state change in a condition. Assigning the result to a named local first (const wasSizing = cancelSurveyRectangleSizing()) reads better and makes the short-circuit ordering explicit — which matters here, because the short-circuit is what stops Escape from cancelling sizing when no survey is being created.
11.3 — localFrame becomes a third copy of the same projection — nit (carried from round 1)
src/libs/map/local-frame.ts is the right extraction, and exporting earthRadiusMeters from src/libs/mission/general-estimates.ts for it is justified by a call site in this same PR. But polygonAreaSquareMeters and centroidLatLng in that same file — the file the PR already edits — still hand-roll the identical equirectangular conversion inline. AGENTS.md ("Reuse before reinventing") wants that living once. Left as a nit because collapsing them is beyond this PR's stated scope and scope discipline cuts the other way; worth a follow-up.
11.4 — Submenu flip side is decided from a hard-coded width and a non-reactive read — nit (carried from round 1)
const shapeMenuWidth = 160
const shapeMenuOpensLeft = computed(
() => clampedPosition.value.x + menuWidth.value + shapeMenuWidth > window.innerWidth
)in ContextMenu.vue. shapeMenuWidth is a magic constant that has to be kept in sync with the submenu's actual CSS width by hand, and window.innerWidth inside a computed is not tracked by Vue — resizing the window while the menu is open leaves the flip decision stale. useWindowSize from vueuse (already installed) covers the reactive half.
11.5 — commitDimensions rewrites the dimension the user did not touch — nit (carried from round 1)
const commitDimensions = async (): Promise<void> => {
if (length.value == null || width.value == null) return
emit('update:dimensions', { length: length.value, width: width.value })
...
}Editing length emits both length and width, with the untouched one round-tripped through the same 0.1 m rounding readDimensions applies, so it can shift by up to a tenth of a metre for no reason the user initiated. Emitting only the field that changed avoids it.
Sections with nothing to report (6)
2. Persistence & User Data — ✅ (no persisted key is added, reshaped or removed — the diff contains no useBlueOsStorage call, no settings-management.ts import and no cockpit-* key; the one store field it writes, interfaceStore.configPanelVisible, is session UI state whose own default at src/stores/appInterface.ts:73 is untouched, and its runtime flip is judged under 6.1; survey shape, phase, dimensions, extent values and drag state are all composable-scoped refs discarded on unmount)
3. AGENTS.md Adherence — ✅ (no package.json change, so no new dependency and no ordering question; every value destructured from the five new composables has a call site in this PR — clearPendingPoint at :2731, pendingDrawnPoint at :2946, touchDrawingSwallowsClick at :3152, lockedExtent in useSurveyRectangleDrawing.ts:1565 — so nothing is groundwork, and the one widened export, earthRadiusMeters, is consumed by local-frame.ts here; each new interface and property carries a JSDoc block with typed @param/@returns as jsdoc/require-jsdoc's TSInterfaceDeclaration/TSPropertySignature contexts demand, none empty or filler; rectangle and edge geometry went to src/libs/map/, reactive orchestration to src/composables/map/; no renames, no import or hook reorders, no formatter-only reflow)
4. Security — ✅ (no network call, no encoded blob, no eval/Function()/v-html, no environment variable or credential, no new dependency, and no change to scripts/, .github/, src/electron/ or any build file — all 18 changed paths are under src/; the one HTML-string construction, handleIcon in useRectangleHandles.ts, interpolates only the two literal title strings its own module passes; identifiers scanned for hidden Unicode and homoglyphs, none found)
8. Commit Hygiene — ✅ (14 commits, of which 6 carry [drop] and are replicated from sibling PR #2977 — the stacked-PR case, disclosed in the PR body with exactly the remedy the rule asks for, a rebase away once that PR merges; the 8 commits this PR owns are one logical change each, scope-prefixed in the repository's own dominant style — composables: map:, libs: map:, mission-planning: — with no wip, no un-squashed fixup!, no commit reverting or reimplementing an earlier one, and no #N or closing keyword in any subject)
9. Tests — ✅ (no test file appears in pr.diff at all, so none was removed or weakened — the two test files listed in incremental.diff are master commits pulled in by the rebase and are not this PR's; the four new src/libs/map/ modules import no Vue and are independently testable by construction)
10. Documentation — ✅ (nothing here differs between Lite and Standalone — no window.electronAPI, no electron-* import, no native dialog or filesystem call; the one platform branch, isTouchDevice() in src/libs/utils.ts, reads window.matchMedia('(pointer: coarse)') and works identically in both builds — so no README parity table entry is owed; JSDoc on the new public surface is covered under section 3)
Generated by Claude. This is advisory; a human reviewer must still approve.
|
@ArturoManzoli as this is a new feature I will be leaving it to be reviewed/merged after our 1.19 beta cycle ends, as discussed on Slack. |
To be merged after Mission Planning: Fix mission planning experience on touchscreen, improving gestures, adding edge dragging and typed distances #2977. Merging it will collapse the [drop] commits at the bottom of this branch.
Survey areas can be drawn as a rectangle: two clicks lay one edge, the preview then follows the pointer for the width and the side it extends to, and a third click locks a shape with square corners.
Length and width read out on the map while it is being drawn, and stay typeable in the survey panel afterwards, so retyping either rebuilds the rectangle around the same edge.
A rectangle still being drawn can be moved by the handle on its first corner and turned by the one on its second, and dragging one of its edges slides that edge along its own normal, so the shape stays rectangular.
Survey lines start out parallel to the rectangle's longer axis, and stay put once the scan-direction dial, or a reshape by hand, takes the angle over.
The result is an ordinary survey area, so line spacing, turnaround distance, crosshatch and entry-point rotation all apply to it unchanged.
The format is picked from a row at the top of the survey panel, or from the map's right-click menu: clicking "Create survey" draws free form, and hovering it (holding it, on a touchscreen) offers the choice.
Once an area is started, the other format is locked out, so the two shapes cannot be mixed in a single survey.
Closes #2894