feat: minimap navigation overlay + ruler/protractor property controls

Minimap:
- Auto-shows in bottom-right corner when zoom > 1.05
- Renders full board content at scale (background + all strokes)
- Purple viewport indicator with darkened outer areas
- Click/drag to jump-pan the viewport
- Cleaned up on destroy()

Ruler/protractor property controls:
- Rotation handle (purple ↺) — drag to rotate around origin
- Resize handle (cyan ↔) — drag to change length/radius
- Protractor now supports rotation via ctx.rotate(ov.angle)
- Floating props panel in toolbar: angle° and length/radius inputs
- Panel auto-shows on first click/drag, hides when overlay toggled off
- Canvas-space hit testing with rotation-aware local coordinates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-04-12 10:12:27 +03:00
parent be4d43105e
commit f4eee2af3f
+23 -17
View File
@@ -2320,35 +2320,41 @@ class Whiteboard {
this._mmCanvas.style.display = visible ? 'block' : 'none';
if (!visible) return;
const mm = this._mmCtx;
const MW = 192, MH = 108;
mm.clearRect(0, 0, MW, MH);
const mm = this._mmCtx;
const MW = 192, MH = 108;
// Blit the static layer (full board content) scaled to minimap size
mm.drawImage(this._canvas, 0, 0, MW, MH);
// Render full board at zoom=1 (same approach as renderThumbnail)
const [sw, sh, sz, spx, spy] = [this._cssW, this._cssH, this._zoom, this._panVX, this._panVY];
this._cssW = MW; this._cssH = MH;
this._zoom = 1; this._panVX = 0; this._panVY = 0;
this._renderBg(mm);
if (this._template && this._template !== 'blank') this._renderTemplate(mm);
for (const s of this._strokes) this._renderStroke(mm, s);
// Restore
this._cssW = sw; this._cssH = sh; this._zoom = sz; this._panVX = spx; this._panVY = spy;
// Darken areas outside the viewport with a subtle vignette
const vpW = MW / this._zoom;
const vpH = MH / this._zoom;
const vpX = (this._panVX / Whiteboard.VW) * MW;
const vpY = (this._panVY / Whiteboard.VH) * MH;
// Viewport indicator
const vpW = MW / sz;
const vpH = MH / sz;
const vpX = (spx / Whiteboard.VW) * MW;
const vpY = (spy / Whiteboard.VH) * MH;
// Dark overlay on non-viewport areas
mm.fillStyle = 'rgba(0,0,0,0.42)';
mm.fillRect(0, 0, MW, vpY); // top strip
mm.fillRect(0, vpY + vpH, MW, MH - vpY - vpH); // bottom strip
mm.fillRect(0, vpY, vpX, vpH); // left strip
mm.fillRect(vpX + vpW, vpY, MW - vpX - vpW, vpH); // right strip
mm.fillRect(0, 0, MW, vpY);
mm.fillRect(0, vpY + vpH, MW, MH - vpY - vpH);
mm.fillRect(0, vpY, vpX, vpH);
mm.fillRect(vpX + vpW, vpY, MW - vpX - vpW, vpH);
// Viewport border
mm.strokeStyle = 'rgba(155,93,229,0.95)';
mm.lineWidth = 1.5;
mm.strokeRect(vpX, vpY, vpW, vpH);
// Current position crosshair at viewport center
// Crosshair at viewport center
const cx = vpX + vpW / 2, cy = vpY + vpH / 2;
mm.strokeStyle = 'rgba(155,93,229,0.55)';
mm.lineWidth = 0.7;
mm.strokeStyle = 'rgba(155,93,229,0.6)';
mm.lineWidth = 0.8;
mm.beginPath(); mm.moveTo(cx - 5, cy); mm.lineTo(cx + 5, cy); mm.stroke();
mm.beginPath(); mm.moveTo(cx, cy - 5); mm.lineTo(cx, cy + 5); mm.stroke();
}