Repository navigation
Conversation
Each pointer move in the Draw tool copied every point, rescanned them through Math.min/Math.max spreads and rebuilt the whole SVG path. Points are now appended in place, bounds widen per point, and the stroke is drawn as a run of 100-segment paths so only the last one is rewritten. Fixes pingdotgg#8059 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — This is a focused desktop annotation performance fix that bounds SVG path updates and replaces repeated bounds scans with constant-time updates while preserving the existing annotation data and curve. The accompanying tests cover equivalence with the prior geometry and very long strokes. You can add or adjust custom eligibility rules. Learn more. |
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Incremental stroke geometry and validation apps/desktop/src/preview/AnnotationStroke.ts, apps/desktop/src/preview/AnnotationStroke.test.ts |
New helpers initialize stroke geometry, append points, update bounds, split paths at 100 segments, and return padded bounds. Tests compare incremental geometry with full-rescan references and check a 250,000-point stroke. |
Preview draw-path integration apps/desktop/src/preview/PickPreload.ts |
The preview draw path uses the geometry helpers, stores strokes in SVG groups, creates a new path when a segment closes, and removes the group for strokes with at most one point. |
Estimated code review effort: 3 (Moderate) | ~20 minutes
Severity of issue fixed: Medium
Suggested reviewers: macroscopeapp
Merge Risk: 
Minimal · up to c408a
-
No actionable merge-blocking issue was identified; the change is ready for normal merge checks.
Pre-merge checks: 
4
-
Full details: Linked Issues check
-
Explanation
The changes address the active coding requirements in [
#8059].AnnotationStroke.tsappends points in place, updates min/max bounds from the new point, and removes unboundedMath.min(...array)andMath.max(...array)calls. It splits the SVG stroke into bounded path chunks and updates only the current chunk.PickPreload.tsstores the geometry points on the stroke target, preserves the stroke group ID for erase behavior, and removes empty groups on pointer-up.AnnotationStroke.test.tscompares joined paths and bounds with the full-rescan behavior and exercises 250,000 points.
Full details: Out of Scope Changes check
-
Explanation
The changed files contain only the stroke geometry implementation, its integration into the desktop annotation draw flow, and focused automated tests. The changes support [
#8059] and do not add unrelated product behavior.
Full details: Description check
-
Explanation
The pull request description includes all required sections. It clearly states the problem, explains the implementation, documents scope and approval rationale, and provides focused tests, manual verification, performance results, screenshots, limitations, and agent attribution.
-
Finishing Touches
-
🧪 Generate unit tests (beta)
-
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Problem
With the Draw tool in the desktop preview's annotation mode, every pointer move copied all points drawn so far, mapped them into two coordinate arrays, spread those into
Math.min/Math.max, and rebuilt the whole SVG path string. One stroke of n points cost O(n²), and the spread threwRangeErroronce a stroke outgrew the engine's argument limit.Fixes #8059
Change
Stroke geometry moves into
apps/desktop/src/preview/AnnotationStroke.ts, a small module with no Electron imports so it can be tested directly. Each move now does constant work:<path>elements of up to 100 curve segments each, grouped under one<g data-stroke-id>. Only the current chunk is extended. A boundary move finalizes that chunk and starts a new short path, keeping the path text parsed per move bounded. Each new path starts at the exact point where the previous one ended, preserving the curve and smoothing.Stored
pointsandboundsand the smoothing math match the old code. Erase still finds the stroke bydata-stroke-id, now on the group.Rejected alternatives:
requestAnimationFrame. Chromium already delivers about onepointermoveper frame (about 1,170 moves in each 20-second stroke below, at 60 frames per second), so it would save nothing.dstill makes Chromium parse the whole path on every move.Scope and approval
Bug fix for #8059, which has no maintainer triage yet. It fits the focused obvious-bug route: each pointer move in a Draw stroke costs more as the stroke grows, and the bounds spread can throw on a long stroke. The fix changes no stored points, bounds, smoothing or drawing flow. The same approach in #8270 was closed only for missing desktop evidence (comment), and this PR adds that evidence. If you would rather reopen #8270, I'm happy to close this one.
Verification
vp test run apps/desktop/src/preview/AnnotationStroke.test.ts: 2 passed. One replays a 230-point stroke and checks at every length that the joined paths and bounds equal the old full-rescan output. The other draws 250,000 points. With the module swapped for base's path and bounds code, both fail: base makes one path, and its 250,000-point bounds throwRangeError: Maximum call stack size exceededatMath.min(...xs).The focused test against base's code:
The same test on the candidate:
Desktop on Linux x64 (Electron 44.9.3): production builds (
vp run build:desktop) of base and candidate, run under Xvfb with a copy of real user data. In a thread's Browser panel I opened a local page, turned on Annotate, picked Draw and drew the same scripted stroke with real X pointer input. Metrics come from the preview page over CDP, three runs per build at similar host load, plus one stroke twice as long:<path>elementsdBase script time grew 2.8x when the stroke doubled. Candidate time doubled. At these lengths neither build drops frames on this machine apart from one host stall in a base run, so this fixes cost growth, not a visible stutter.
The finished stroke on base:
The same stroke on the candidate:
Zoomed 4x at three points where one candidate path hands off to the next (top row base, bottom row candidate). The joins render the same:
Both builds drawing the stroke, played at 2x (base left, candidate right):
Erase still removes the whole stroke: over CDP the page held 24 stroke paths in one group before one Erase click and none after.
Erase on the candidate, before and after one click on the stroke:
After Attach on the candidate, the annotation appears in the composer:
vp run check:changed main: format, lint, knip, typecheck and tests pass.Not checked:
RangeErrorinside the app. In Node the spread fails between 125,000 and 130,000 arguments, which at 60 points per second is over half an hour of continuous drawing. The unit test covers it.Prior PRs
Generated with Claude Opus 5.5 in T3 Code Nightly