Skip to content

Mission Planning: Add a rectangle survey tool with typed dimensions - #2975

Open
ArturoManzoli wants to merge 14 commits into
bluerobotics:masterfrom
ArturoManzoli:2894-rectangle-survey-tool-typed-dimensions
Open

Mission Planning: Add a rectangle survey tool with typed dimensions#2975
ArturoManzoli wants to merge 14 commits into
bluerobotics:masterfrom
ArturoManzoli:2894-rectangle-survey-tool-typed-dimensions

Conversation

@ArturoManzoli

@ArturoManzoli ArturoManzoli commented Aug 26, 2026

Copy link
Copy Markdown
Contributor
  • 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.

Survey format row at the top of the survey panel

Survey format choice in the map context menu

Closes #2894

@github-actions

Copy link
Copy Markdown
⚠️ IMPORTANT FIXES REQUIRED (Automated PR Review — round 1)

9 open findings — 1 major, 3 minor, 5 nit.

This adds a second way to draw a survey area. Instead of clicking a corner at a time to build a free-form shape, you can pick "Rectangle", click twice to lay down one side, move the mouse to pull the rectangle out to the size you want, and click once more to finish — with the length and width shown live and then editable as numbers in the side panel. Once a shape exists you can also grab any of its sides and drag it to make the area bigger or smaller, instead of moving corners individually. The shape choice is offered both from the side panel and from a submenu on the map's right-click menu, and the measurement bubbles that follow the cursor while you drag are now the same piece of code the existing distance tool uses. Two smaller behaviour changes ride along: "Clear Path" now puts you back into point-placement mode rather than leaving you stuck, and the "no valid path" warning no longer fires on every intermediate frame while you are still dragging.

What still needs attention

# 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 moveverified. consumeSurveyClick in src/composables/map/useSurveyRectangleDrawing.ts runs a 'idle' → 'baseline' → 'sizing' phase machine, with rectangleFromBaselineAndCursor (src/libs/map/survey-rectangle.ts) building the corners.
  • Length and width can be typed and the rectangle rebuildsverified. applyDimensions clamps to minExtentInMeters = 1 / maxExtentInMeters = 100000 and rebuilds through rectangleCorners; the inputs live in the new src/components/mission-planning/SurveyShapeControls.vue.
  • The scan-line angle follows the rectangle unless the user overrides itverified, and the math is right. rectangleLinesAngle returns atan2(Δlng, Δlat); createSurveyPath passes 90 − surveyLinesAngle into generateSurveyPath (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 reshapeverified. src/composables/map/useSurveyEdgeDragging.ts, container-level pointer events with setPointerCapture, grabToleranceInPixels = 10, dragThresholdInPixels = 4.
  • The shape can be picked from the map context menuverified. src/components/mission-planning/ContextMenu.vue gains a hover/focus flyout plus a 450 ms long-press path for touch, emitting setSurveyShape.
  • The drag-measure pill overlay is now sharedverified, and it is a net deletion: useDragMeasureOverlay.ts goes 17+/47− by delegating to the new useMeasurePillOverlay.ts.
  • "Clear Path" re-arms vertex placementverified, commit 4fdd2b4b0, clearSurveyPathByUser now sets isDrawingSurveyPolygon.value = true.
  • Invalid-path warnings only fire on releaseverified, commit 51ad4f57e, createSurveyPath now gates the dialog on a new isReshapingSurveyPolygon computed.

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 the isReshapingSurveyPolygon guard there, not a guard per drag handler. Both are the shared-function fix AGENTS.md asks 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
onSizingMouseMoverenderPreviewrenderMeasurePills leaflet map mousemove per frame or pointer event
onPointerMoveedgeAtclosestPolygonEdge / 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

  1. The rectangle owns surveyLinesAngle only until the user takes it over. useSurveyRectangleDrawing keeps derivedCorners and drops it once the user sets the angle by hand. Sites that can set the angle: the surveyLinesAngleDisplay setter, onSurveyLinesAngleChange, performUndo, undoGenerateWaypoints, and the v-model:angle on ScanDirectionDial (MissionPlanningView.vue:158). The PR adds releaseSurveyLinesAngle() to the first four; the fifth is inert because ScanDirectionDial never emits update:angle — it emits surveyLinesAngle, which routes through onSurveyLinesAngleChange. Enumeration is exhaustive and the PR covers all of it.
  2. Every mutation of surveyPolygonVertexesPositions while a survey is being created pushes an undo snapshot first. Held by addSurveyPoint (:3439), onPolygonMouseDown (:2186), the vertex dragstart, the edge-drag onDragStart, and the new onShapeChanged. Broken by the rectangle draw and typed-resize paths — see 1.1.
  3. A phase of 'baseline' with zero vertices must not deadlock the tool. Traced: consumeSurveyClick falls through and addSurveyPoint runs, 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 surveymajor

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 rectangleminor

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: stopSizingsetVerticesonRectangleDrawnrebuildSurveyPolygonFromPositions (: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 rectangleminor

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 triggersminor

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 twicenit

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 testnit

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 projectionnit

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 readnit

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 touchnit

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.
@ArturoManzoli
ArturoManzoli force-pushed the 2894-rectangle-survey-tool-typed-dimensions branch from 51ad4f5 to f6e921b Compare August 27, 2026 15:44
@ArturoManzoli

Copy link
Copy Markdown
Contributor Author

/review

@github-actions

Copy link
Copy Markdown
⚠️ IMPORTANT FIXES REQUIRED (Automated PR Review — round 2)

13 open findings — 1 major (1.1), 6 minor, 6 nit. 0 closed since round 1.

This adds a second way to draw a survey area. Instead of clicking a corner at a time to build a free-form shape, you pick "Rectangle", click twice to lay down one side, move the pointer to pull the rectangle out to the size you want, and click once more to finish. Since the last round the tool has grown a keyboard path — the length and width can now be typed straight onto the map while you draw, into an invisible field sitting under the measurement bubble — plus two drag handles on the first two corners that move and turn the finished rectangle, and a rule that locks you into whichever format you started the area in. The panel that holds the format buttons and the survey settings no longer opens on its own when you start a survey; it folds away behind a small tab on the right edge labelled "Vertexes".

What still needs attention

# 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 51ad4f5f6e921b

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.1onRectangleDrawn and onRectangleResized (src/views/MissionPlanningView.vue, the useSurveyRectangleDrawing options object) still call rebuildSurveyPolygonFromPositions() with no pushSurveyPolygonSnapshot() in front of it, and applyDimensions still writes vertices directly. The round did add snapshot pushes to two other paths — the new handle drag and the edge drag both do if (!isSizingRectangle.value) pushSurveyPolygonSnapshot() — which is the right pattern applied everywhere except the two sites the finding named.
  • 1.2rebuildSurveyPolygonFromPositions (:2473) still never calls updateLiveSurveyAreaLabel, and createSurveyPath does not either.
  • 1.3 — the two panel inputs in SurveyShapeControls.vue still commit on @change only. The new on-map field (MeasureExtentInput.vue) does event.stopPropagation() on every key, but that is a different control.
  • 5.1maxExtentInMeters = 100000 moved from the composable into the new src/libs/map/typed-extent.ts and gained an explanatory comment, but the clamp still bounds the metres and not the work.
  • 11.1 — unchanged, and now wider: useRectangleHandles.ts and useTouchDrawing.ts are 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 cornersverified. consumeSurveyClick (src/composables/map/useSurveyRectangleDrawing.ts) runs an 'idle' → 'baseline' → 'sizing' phase machine; rectangleFromBaselineAndCursor (src/libs/map/survey-rectangle.ts) builds the corners; commitSizing refuses 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 afterwardsverified. renderExtents places one pill on the drawn edge and one on the edge across it via renderMeasurePills; the panel fields live in src/components/mission-planning/SurveyShapeControls.vue and read back from the dimensions computed. Qualified: the panel holding those fields no longer opens when a survey starts — see 6.1.
  • Retyping either rebuilds the rectangle around the same edgeverified. applyDimensions rebuilds from { ...spec, length, width } where spec is specFromCorners of 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 secondverified. src/composables/map/useRectangleHandles.ts holds a snapshot of the corners at dragstart (held) so each move is measured from there rather than compounding, and applies rectangleTranslatedBy / rectangleRotatedAbout about axisCorner(held).
  • Dragging an edge slides it along its own normal so the shape stays rectangularverified. useSurveyEdgeDragging.ts decides holdEdgeSquare = rectangleSpec(points.map(asCoordinates)) !== null once per drag and hands it to draggedEdgeEndpoints; 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 oververified. derivedCorners is compared against the live vertices (areSameCorners) before the angle is allowed to follow, and releaseSurveyLinesAngle() is now also wired into performUndo.
  • The result is an ordinary survey area, so spacing, turnaround, crosshatch and entry rotation all applyverified. commitSizing ends by writing four entries into surveyPolygonVertexesPositions; createSurveyPath consumes 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 choiceverified in code. handleSurveyEntryClick falls through to handleSetSurveyShape('free-form'), and startShapeMenuLongPress opens 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 outverified. SurveyShapeControls takes :locked="surveyPolygonVertexesPositions.length > 0" and disables the non-current button; offersShapeChoice = !props.isCreatingSurvey removes 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] commitsverified against pr.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 clickonMapClickplaceSurveyPolygonPoint per user action
commitSizing / applyDimensions / setShape / cancelSizing third sizing click; SurveyShapeControls @change and buttons; Escape per user action
onSizingMouseMoverenderPreviewrenderExtentsrenderMeasurePills + 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
onPointerMoveedgeAtclosestPolygonEdge / setCursor (useSurveyEdgeDragging.ts) map container pointermove per frame or pointer event
onPointerDown / onPointerUp / onPointerLeave (same file) map container pointer events per user action
onAxisDrag / onTurnDragrectangleTranslatedBy / 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) → aimAtfireMapMouseMove 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 / onShapeChangedrebuildSurveyPolygonFromPositions 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

  1. Every mutation of surveyPolygonVertexesPositions while a survey is being created pushes an undo snapshot first. Sites that can mutate it: addSurveyPoint (:3439), onPolygonMouseDown (:2186), the vertex dragstart, the edge-drag onDragStart, the new handle onDragStart, onShapeChanged, and setVertices inside useSurveyRectangleDrawing. The PR covers the first six — the handle path is new this round and does it correctly — and leaves the seventh, reached from commitSizing and applyDimensions, uncovered. Enumeration is exhaustive; the PR covers all but the two sites 1.1 names.
  2. The rectangle owns surveyLinesAngle only until the user takes it over. Held by derivedCorners plus areSameCorners. Sites that set the angle: the surveyLinesAngleDisplay setter, onSurveyLinesAngleChange, performUndo, undoGenerateWaypoints, and ScanDirectionDial's v-model:angle — the last is inert because the dial emits surveyLinesAngle, which routes through onSurveyLinesAngleChange. All four live sites call releaseSurveyLinesAngle(). Covered; no finding.
  3. No invisible extent field may be left over the map once there is nothing to type into. extentBoxes returns [] whenever !extentInputsOpen.value, so a closed field renders nothing at all; stopSizing clears both rectangle targets and calls closeExtentInputs; handleMapMouseMove clears 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 surveymajor (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 commitSizingsetVertices 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 rectangleminor (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: commitSizingstopSizingsetVerticesonRectangleDrawnrebuildSurveyPolygonFromPositions (: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 rectangleminor (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 triggersminor (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 (applyExtentsetExtentValuelockedExtent), 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. SurveyShapeControls locks 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's shape ref 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 layernit

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 implementationminor

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 pathminor

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 twicenit (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 testnit (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 projectionnit (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 readnit (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 touchnit (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.

@rafaellehmkuhl

Copy link
Copy Markdown
Member

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mission planning: add a rectangle survey tool with typed dimensions and lines parallel to the longer axis

2 participants