Network map enhancements and fixes
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Vertical +/−/fit-to-view zoom controls for the topology canvas.
|
||||
/// Requires a [TransformationController] shared with the [InteractiveViewer].
|
||||
class ZoomControls extends StatelessWidget {
|
||||
const ZoomControls({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.canvasSize,
|
||||
required this.viewportSize,
|
||||
});
|
||||
|
||||
final TransformationController controller;
|
||||
|
||||
/// Full canvas size (from [computeSugiyamaLayout] result).
|
||||
final Size canvasSize;
|
||||
|
||||
/// Viewport widget size (the screen area the canvas is displayed in).
|
||||
final Size viewportSize;
|
||||
|
||||
static const double _zoomIn = 1.25;
|
||||
static const double _zoomOut = 0.8;
|
||||
static const double _minScale = 0.2;
|
||||
static const double _maxScale = 2.5;
|
||||
|
||||
void _applyZoom(double factor) {
|
||||
final current = controller.value.getMaxScaleOnAxis();
|
||||
final next = (current * factor).clamp(_minScale, _maxScale);
|
||||
final scale = next / current;
|
||||
final cx = viewportSize.width / 2;
|
||||
final cy = viewportSize.height / 2;
|
||||
// Compose: T(cx,cy) * Scale(s) * T(-cx,-cy) * currentTransform
|
||||
final result = Matrix4.translationValues(cx, cy, 0);
|
||||
result.multiply(Matrix4.diagonal3Values(scale, scale, 1.0));
|
||||
result.multiply(Matrix4.translationValues(-cx, -cy, 0));
|
||||
result.multiply(controller.value);
|
||||
controller.value = result;
|
||||
}
|
||||
|
||||
void _fitToView() {
|
||||
if (canvasSize.isEmpty) return;
|
||||
final scaleX = viewportSize.width / canvasSize.width;
|
||||
final scaleY = viewportSize.height / canvasSize.height;
|
||||
final scale = (scaleX < scaleY ? scaleX : scaleY).clamp(_minScale, _maxScale);
|
||||
final tx = (viewportSize.width - canvasSize.width * scale) / 2;
|
||||
final ty = (viewportSize.height - canvasSize.height * scale) / 2;
|
||||
// Build: T(tx,ty,0) * Scale(s,s,1) — maps canvas (0,0) → (tx,ty) on screen
|
||||
final result = Matrix4.translationValues(tx, ty, 0);
|
||||
result.multiply(Matrix4.diagonal3Values(scale, scale, 1.0));
|
||||
controller.value = result;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Material(
|
||||
color: cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
elevation: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_ZoomButton(
|
||||
icon: Icons.add,
|
||||
tooltip: 'Zoom in',
|
||||
onTap: () => _applyZoom(_zoomIn),
|
||||
),
|
||||
_Divider(color: cs.outlineVariant.withValues(alpha: 0.4)),
|
||||
_ZoomButton(
|
||||
icon: Icons.remove,
|
||||
tooltip: 'Zoom out',
|
||||
onTap: () => _applyZoom(_zoomOut),
|
||||
),
|
||||
_Divider(color: cs.outlineVariant.withValues(alpha: 0.4)),
|
||||
_ZoomButton(
|
||||
icon: Icons.fit_screen_outlined,
|
||||
tooltip: 'Fit to view',
|
||||
onTap: _fitToView,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ZoomButton extends StatelessWidget {
|
||||
const _ZoomButton({
|
||||
required this.icon,
|
||||
required this.tooltip,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String tooltip;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Icon(icon, size: 18, color: cs.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Divider extends StatelessWidget {
|
||||
const _Divider({required this.color});
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Divider(height: 1, color: color),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user