Skip to content

feat: auto shape completion#1798

Open
andrinwinzap wants to merge 2 commits into
flxzt:mainfrom
andrinwinzap:feat/auto-shape-completion
Open

feat: auto shape completion#1798
andrinwinzap wants to merge 2 commits into
flxzt:mainfrom
andrinwinzap:feat/auto-shape-completion

Conversation

@andrinwinzap

Copy link
Copy Markdown

Automatic shape completion for the brush pen

Adds opt-in, OneNote-style shape recognition to the brush pen. When enabled, holding
the pen still at the end of a stroke replaces the drawn ink with the clean geometric
shape it resembles.

fixes #120

Motivation

Drawing exact shapes freehand is difficult, and switching to the shaper pen for a
single rectangle interrupts the flow of writing. Letting the brush recognize shapes
on demand keeps the user in one tool. Recognition is deliberately not run on every
pen-up: doing so makes the brush unpredictable, since any stroke that happens to look
like a line or a loop would be silently rewritten. Requiring an explicit hold makes
the replacement something the user asks for rather than something that happens to them.

Changes

rnote-compose: new shaperecognition module.
recognize_shape(&PenPath) -> Option<Shape> resamples the stroke to equal arc-length
spacing, classifies it as open or closed from the ratio of the endpoint gap to the
perimeter, and then dispatches:

  • open strokes are fitted against their chord and accepted as a Line
  • closed strokes are scored for "cornerness" from the distribution of turn angles
    along the loop; low-corner strokes are fitted as an Ellipse through the second
    moments of the outline, and high-corner strokes have their corners extracted and
    are validated as a Polygon
  • a four-corner polygon with near-right angles is emitted as a Rectangle,
    snapping to axis-alignment when it is within a few degrees

Recognized shapes are lines, circles, ellipses, rectangles (including rotated ones),
and closed polygons of up to six corners. Every fit is validated against the original
stroke by residual/deviation thresholds, so strokes that do not clearly resemble a
shape are left as ink. The module is dependency-free and covered by unit tests for
each shape class, plus rejection tests for scribbles and degenerate strokes.

rnote-engine: hold-to-trigger in the brush pen.
While drawing, the brush tracks an anchor position and restarts a one-off timeout task
whenever the pen leaves a small radius around it. When the pen rests for the hold
duration, the task emits a new EngineTask::BrushRecognizeShape, which attempts
recognition and swaps the in-progress stroke for the recognized shape while the pen is
still down. Subsequent motion is swallowed until pen-up, so the shape does not get
re-drawn over.

Two constants govern the interaction, both in brush.rs:

  • HOLD_DURATION = 700ms — long enough not to fire during the natural pause at the
    end of a stroke, short enough not to feel like waiting.
  • HOLD_RADIUS_SURFACE = 6.0 (surface coords) — tolerates hand tremor and pen
    jitter while resting, without swallowing slow deliberate movement.

Both are hardcoded rather than exposed in the UI, to avoid adding a preference for a
value most users will not tune. Happy to surface them if you would prefer.

rnote-ui: a "Shape Recognition" group in the brush page with a single
Recognize Shapes switch. The feature is off by default, so existing behaviour is
unchanged unless the user opts in. The setting is stored as
BrushConfig::shape_recognition_enabled; BrushConfig already carries
#[serde(default)], so configs written by older versions continue to load.

Recognized shapes are drawn with the brush's current colour and width. Textured strokes
fall back to a smooth style with a constant pressure curve, since shapes cannot be
composed with the textured style.

Screenshots / screen capture

Screencast_20260709_163543.webm

Testing

  • cargo fmt --check is clean and cargo clippy --workspace --all-targets reports no
    warnings.
  • cargo test --workspace passes, including 8 new unit tests in
    rnote-compose::shaperecognition covering line, circle, flat ellipse, axis-aligned
    rectangle, rotated rectangle and triangle recognition, plus rejection of scribbles
    and strokes below the minimum size.

AI disclosure

This contribution was developed with the assistance of LLM/GenAI tools.

Add OneNote-style automatic shape recognition: when enabled, drawn
brush strokes that closely resemble a geometric shape are replaced
with the clean shape upon finishing the stroke.

Recognized are lines, circles, ellipses, rectangles (also rotated)
and closed polygons with up to six corners. Recognition works by
resampling the stroke to equal arc-length spacing, classifying it as
open or closed via the endpoint gap, and then either fitting a line
to the chord, fitting an ellipse through the second moments of the
outline, or detecting corners from the distribution of turn angles
along the loop and validating the resulting polygon against the
stroke.

The feature is off by default and toggled in the brush config popover.
Instead of attempting to recognize a shape every time the pen is
lifted, recognition is now triggered like in OneNote: by holding the
pen still at the end of a drawn stroke. Lifting the pen normally
always keeps the drawn ink.

While drawing, the brush tracks an anchor position and restarts a
one-off timeout task whenever the pen moves out of a small radius
around it. When the pen rests for the hold duration (700ms), the task
sends an engine task which attempts the recognition and replaces the
drawn stroke with the recognized shape while the pen is still down.
Subsequent events are swallowed until the pen is lifted.
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.

Add shape recognition

1 participant