Skip to content

Add freehand annotations layer#31

Merged
haan merged 16 commits into
mainfrom
feature/annotations
May 14, 2026
Merged

Add freehand annotations layer#31
haan merged 16 commits into
mainfrom
feature/annotations

Conversation

@haan
Copy link
Copy Markdown
Owner

@haan haan commented May 14, 2026

Summary

  • Adds an SVG overlay annotation layer that renders in flow coordinates, so strokes and text move and zoom with the diagram
  • Supports pen, marker (semi-transparent), text, and eraser tools with per-tool settings (color, size, opacity) persisted to localStorage
  • Annotations are view-specific (conceptual/logical/physical), saved in the .mdlz file, and integrated with the undo/redo history
  • Toolbox with keyboard shortcuts (V/P/M/T/E), space-hold to temporarily pan, and text selection/drag/inline editing
  • Feature is opt-in via Settings → Enable annotations

Test plan

  • Settings → Enable annotations shows toolbox; setting persists on reload
  • Pen and marker draw strokes that move and zoom with the diagram
  • Eraser whole and partial modes work correctly
  • Text tool: click to place, Enter/blur to commit, click to select, double-click to edit, Delete to remove
  • Toolbox color/size settings apply to new annotations and to selected text items
  • Annotations are view-specific — switching views shows that view's annotations
  • Save and reload a .mdlz file — annotations restored correctly
  • Undo/redo covers annotation changes alongside diagram changes
  • PNG export includes annotations
  • Space+drag pans the diagram while an annotation tool is active
  • npm test passes

Laurent Haan added 16 commits May 13, 2026 16:57
Adds an optional annotations layer (toggled via Settings → Enable annotations) that renders on top of the UML diagram and moves/zooms with it. Annotations are view-specific (conceptual/logical/physical) and saved in the .mdlz file.

Tools: pen (color, thickness), marker (color, thickness, opacity), text label (color, font size), eraser (size, whole/partial mode). Each drawing tool has a color preset row with 8 swatches and a custom color picker. The toolbox uses Bootstrap Icons.

Undo/redo is unified with the existing Edit menu history: every annotation mutation pushes a combined snapshot of nodes, edges, model name and annotations, so Ctrl+Z undoes both diagram and annotation changes in a single stack.
The cursor now reflects the active annotation tool and its configured
size: a scaled SVG circle for pen, marker (with semi-transparent fill),
and eraser; text cursor for the text tool; default arrow for pointer.
Replace window.prompt with an inline textarea that appears directly on
the canvas. Supports multiline text via Shift+Enter, auto-resizes as
you type, and commits on Enter or blur. Double-clicking an existing
text item opens it for inline editing in place. Rendered text now
splits on newlines using tspan elements.
- Align textarea baseline with click position by shifting the
  foreignObject up by ~85% of the font size; change rendered SVG
  text to dominantBaseline="alphabetic" to match
- Single click on an existing text label in text mode now opens it
  for inline editing instead of placing a new label on top
- While a text input is open, clicking elsewhere only commits the
  current input; it no longer immediately opens a new one at the
  click position
Two root causes:
- e.preventDefault() was called unconditionally in onPointerDown,
  which blocks browser focus changes and prevents the textarea blur
  from firing when clicking elsewhere. Now only called for drawing
  tools (pen, marker, eraser).
- Clicking on existing SVG text items relied on stopPropagation from
  a child element through React's synthetic event system, which is
  unreliable for SVG. Replaced with data-annotation-id attribute on
  each text element and e.target.closest() detection in the SVG's
  own onPointerDown wrapper.
… for text-item clicks

The handleSvgPointerDown wrapper broke new text placement. Replace it
with onPointerDownCapture on the <g> element: capture phase fires on <g>
before the bubble phase reaches <svg>, so text-item clicks are detected
and consumed there. Clicks on empty canvas target <svg> directly and
never pass through <g>, so they reach onPointerDown unchanged.

Also restore autoFocus on the textarea so focus works reliably inside
SVG foreignObject without relying on programmatic el.focus().
The SVG element is not a guaranteed hit target for transparent empty
areas, so pointer events could silently miss it. Moving all handlers
to the parent div (which always covers inset:0) ensures every click
in the overlay is received. The SVG is kept with pointer-events:all
so its children remain hittable and the <g> capture handler still
fires for text-item clicks before they bubble to the div.
- Restore e.preventDefault() for all tools; rely on explicit blur
  instead of browser-managed focus changes for text commit
- Move all pointer handlers to the overlay div (guaranteed hit surface);
  SVG has no explicit pointer-events so it inherits from the div —
  this fixes pan/zoom in pointer mode where the div is pointer-events:none
- Blur the active textarea explicitly in handlePointerDown before
  e.preventDefault() can suppress it; return without placing new text
- Detect text-item clicks via onPointerDownCapture on <g>; capture
  phase fires before the div bubble handler, stopPropagation prevents
  accidental new-text placement
- TextEditor exposes its textarea via textareaRef so the div handler
  can blur it; editing TextEditor rendered at AnnotationLayer level
  (not inside AnnotationText) to share the same activeTextareaRef
@haan haan merged commit e0fea85 into main May 14, 2026
@haan haan deleted the feature/annotations branch May 14, 2026 07:18
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.

1 participant