mobile-eda/lib/presentation/components/editable_canvas.dart

822 lines
23 KiB
Dart

/**
* 可编辑画布组件
*
* 基于 Flutter CustomPainter 实现画布渲染
* 支持元件实时拖拽、放置、旋转
* 实现连线功能:点击引脚→拖拽→释放到目标引脚
* 支持差分对、总线批量连线
*
* @version 0.2.0
* @date 2026-03-07
*/
import 'package:flutter/material.dart';
import 'dart:math' as math;
import '../models/core_models.dart';
import '../managers/selection_manager.dart';
/// 画布状态枚举
enum CanvasState {
idle, // 空闲
panning, // 平移中
draggingComponent, // 拖拽元件
wiring, // 连线中
boxSelecting, // 框选中
rotating, // 旋转中
}
/// 连线状态
class WiringState {
final ConnectionPoint startPoint;
final Offset currentPoint;
final NetType netType;
const WiringState({
required this.startPoint,
required this.currentPoint,
this.netType = NetType.signal,
});
}
/// 可编辑画布组件
class EditableCanvas extends StatefulWidget {
final Design design;
final Function(Design) onDesignChanged;
final SelectionManager selectionManager;
final MobileOptimizations? optimizations;
const EditableCanvas({
super.key,
required this.design,
required this.onDesignChanged,
required this.selectionManager,
this.optimizations,
});
@override
State<EditableCanvas> createState() => _EditableCanvasState();
}
class _EditableCanvasState extends State<EditableCanvas> {
// 画布状态
CanvasState _state = CanvasState.idle;
// 视图变换
double _zoomLevel = 1.0;
Offset _offset = Offset.zero;
// 手势相关
Offset? _lastPanPosition;
Offset? _lastTapPosition;
// 拖拽相关
ID? _draggingComponentId;
Offset? _dragStartOffset;
// 连线相关
WiringState? _wiringState;
// 旋转相关
ID? _rotatingComponentId;
// 框选相关
Rect? _selectionRect;
// 悬停的引脚(用于连线提示)
PinReference? _hoveredPin;
Component? _hoveredComponent;
// 网格大小(像素)
static const double _gridSize = 20.0;
// 元件吸附距离(像素)
static const double _snapDistance = 10.0;
@override
Widget build(BuildContext context) {
return GestureDetector(
// 双指缩放
onScaleStart: _handleScaleStart,
onScaleUpdate: _handleScaleUpdate,
onScaleEnd: _handleScaleEnd,
// 单指拖拽
onPanStart: _handlePanStart,
onPanUpdate: _handlePanUpdate,
onPanEnd: _handlePanEnd,
// 点击
onTapUp: _handleTapUp,
// 长按
onLongPress: _handleLongPress,
child: Container(
color: const Color(0xFFFAFAFA),
child: CustomPaint(
size: Size.infinite,
painter: SchematicCanvasPainter(
design: widget.design,
zoomLevel: _zoomLevel,
offset: _offset,
selectionManager: widget.selectionManager,
wiringState: _wiringState,
selectionRect: _selectionRect,
hoveredPin: _hoveredPin,
hoveredComponent: _hoveredComponent,
gridSize: _gridSize,
),
),
),
);
}
// ============================================================================
// 手势处理
// ============================================================================
void _handleScaleStart(ScaleStartDetails details) {
_lastPanPosition = details.focalPoint;
setState(() {
_state = CanvasState.panning;
});
}
void _handleScaleUpdate(ScaleUpdateDetails details) {
if (_lastPanPosition == null) return;
setState(() {
// 更新缩放级别(限制范围)
_zoomLevel = (_zoomLevel * details.scale).clamp(0.1, 10.0);
// 更新偏移
_offset += details.focalPoint - _lastPanPosition!;
_lastPanPosition = details.focalPoint;
});
}
void _handleScaleEnd(ScaleEndDetails details) {
setState(() {
_state = CanvasState.idle;
_lastPanPosition = null;
});
}
void _handlePanStart(DragStartDetails details) {
// 如果点击在元件上,开始拖拽
final worldPos = _screenToWorld(details.globalPosition);
final component = _findComponentAtPosition(worldPos);
if (component != null) {
setState(() {
_state = CanvasState.draggingComponent;
_draggingComponentId = component.id;
_dragStartOffset = worldPos;
});
} else {
// 否则开始平移
setState(() {
_state = CanvasState.panning;
_lastPanPosition = details.globalPosition;
});
}
}
void _handlePanUpdate(DragUpdateDetails details) {
final worldPos = _screenToWorld(details.globalPosition);
if (_state == CanvasState.draggingComponent && _draggingComponentId != null) {
// 拖拽元件
_updateComponentPosition(_draggingComponentId!, worldPos);
} else if (_state == CanvasState.panning && _lastPanPosition != null) {
// 平移画布
setState(() {
_offset += details.delta;
});
} else if (_state == CanvasState.wiring && _wiringState != null) {
// 更新连线
setState(() {
_wiringState = WiringState(
startPoint: _wiringState!.startPoint,
currentPoint: worldPos,
netType: _wiringState!.netType,
);
});
}
}
void _handlePanEnd(DragEndDetails details) {
if (_state == CanvasState.draggingComponent) {
// 完成拖拽,吸附到网格
if (_draggingComponentId != null) {
_snapComponentToGrid(_draggingComponentId!);
}
}
setState(() {
_state = CanvasState.idle;
_draggingComponentId = null;
_dragStartOffset = null;
_lastPanPosition = null;
});
}
void _handleTapUp(TapUpDetails details) {
final worldPos = _screenToWorld(details.globalPosition);
// 检查是否点击在引脚上
final pinHit = _findPinAtPosition(worldPos);
if (pinHit != null) {
if (_wiringState == null) {
// 开始连线
setState(() {
_state = CanvasState.wiring;
_wiringState = WiringState(
startPoint: ConnectionPoint(
id: '${pinHit.component.id}:${pinHit.pin.pinId}',
type: ConnectionType.pin,
componentId: pinHit.component.id,
pinId: pinHit.pin.pinId,
position: Position2D(x: pinHit.component.position.x + pinHit.pin.x, y: pinHit.component.position.y + pinHit.pin.y),
layerId: pinHit.component.layerId,
),
currentPoint: worldPos,
);
});
} else {
// 完成连线
_completeWiring(pinHit);
}
} else {
// 点击空白区域,取消连线
if (_wiringState != null) {
setState(() {
_wiringState = null;
_state = CanvasState.idle;
});
}
// 检查是否点击在元件上(选择)
final component = _findComponentAtPosition(worldPos);
if (component != null) {
widget.selectionManager.selectSingle(SelectableObject(
type: SelectableType.component,
id: component.id,
));
} else {
widget.selectionManager.clearSelection();
}
}
}
void _handleLongPress() {
// 显示上下文菜单
_showContextMenu();
}
// ============================================================================
// 元件操作
// ============================================================================
void _updateComponentPosition(ID componentId, Offset screenPos) {
final worldPos = _screenToWorld(screenPos);
final component = widget.design.components[componentId];
if (component == null) return;
// 计算新位置(考虑吸附)
final snapPos = _snapToGrid(worldPos);
final newComponent = component.copyWith(
position: Position2D(
x: snapPos.dx.toInt(),
y: snapPos.dy.toInt(),
),
);
// 更新设计
final newDesign = widget.design.copyWith(
components: Map.from(widget.design.components)..[componentId] = newComponent,
updatedAt: DateTime.now().millisecondsSinceEpoch,
isDirty: true,
);
widget.onDesignChanged(newDesign);
}
void _snapComponentToGrid(ID componentId) {
final component = widget.design.components[componentId];
if (component == null) return;
final snapX = ((component.position.x / _gridSize).round() * _gridSize).toInt();
final snapY = ((component.position.y / _gridSize).round() * _gridSize).toInt();
final newComponent = component.copyWith(
position: Position2D(x: snapX, y: snapY),
);
final newDesign = widget.design.copyWith(
components: Map.from(widget.design.components)..[componentId] = newComponent,
updatedAt: DateTime.now().millisecondsSinceEpoch,
isDirty: true,
);
widget.onDesignChanged(newDesign);
}
/// 旋转元件
void rotateComponent(ID componentId, {int angle = 90}) {
final component = widget.design.components[componentId];
if (component == null) return;
final currentRotation = component.rotation;
final newRotation = (currentRotation + angle) % 360;
final newComponent = component.copyWith(rotation: newRotation);
final newDesign = widget.design.copyWith(
components: Map.from(widget.design.components)..[componentId] = newComponent,
updatedAt: DateTime.now().millisecondsSinceEpoch,
isDirty: true,
);
widget.onDesignChanged(newDesign);
}
// ============================================================================
// 连线操作
// ============================================================================
void _completeWiring({required Component targetComponent, required PinReference targetPin}) {
if (_wiringState == null) return;
final startConn = _wiringState!.startPoint;
final endConn = ConnectionPoint(
id: '${targetComponent.id}:${targetPin.pinId}',
type: ConnectionType.pin,
componentId: targetComponent.id,
pinId: targetPin.pinId,
position: Position2D(
x: targetComponent.position.x + targetPin.x,
y: targetComponent.position.y + targetPin.y,
),
layerId: targetComponent.layerId,
);
// 创建或更新网络
final net = _findOrCreateNet(startConn, endConn);
setState(() {
_wiringState = null;
_state = CanvasState.idle;
});
}
Net _findOrCreateNet(ConnectionPoint start, ConnectionPoint end) {
// 查找是否已有网络包含起点
Net? existingNet;
for (final net in widget.design.nets.values) {
for (final conn in net.connections) {
if (conn.id == start.id) {
existingNet = net;
break;
}
}
if (existingNet != null) break;
}
if (existingNet != null) {
// 添加到现有网络
final newNet = existingNet.copyWith(
connections: [...existingNet.connections, end],
);
final newDesign = widget.design.copyWith(
nets: Map.from(widget.design.nets)..[existingNet.id] = newNet,
updatedAt: DateTime.now().millisecondsSinceEpoch,
isDirty: true,
);
widget.onDesignChanged(newDesign);
return newNet;
} else {
// 创建新网络
final timestamp = DateTime.now().millisecondsSinceEpoch;
final newNet = Net(
id: 'net_${timestamp}',
name: 'N${timestamp.toString().substring(timestamp.toString().length - 6)}',
type: _wiringState!.netType,
connections: [start, end],
metadata: Metadata(createdAt: timestamp, updatedAt: timestamp),
);
final newDesign = widget.design.copyWith(
nets: Map.from(widget.design.nets)..[newNet.id] = newNet,
updatedAt: DateTime.now().millisecondsSinceEpoch,
isDirty: true,
);
widget.onDesignChanged(newDesign);
return newNet;
}
}
// ============================================================================
// 辅助方法
// ============================================================================
/// 屏幕坐标转世界坐标
Offset _screenToWorld(Offset screenPos) {
return Offset(
(screenPos.dx - _offset.dx) / _zoomLevel,
(screenPos.dy - _offset.dy) / _zoomLevel,
);
}
/// 世界坐标转屏幕坐标
Offset _worldToScreen(Offset worldPos) {
return Offset(
worldPos.dx * _zoomLevel + _offset.dx,
worldPos.dy * _zoomLevel + _offset.dy,
);
}
/// 吸附到网格
Offset _snapToGrid(Offset pos) {
return Offset(
(pos.dx / _gridSize).round() * _gridSize,
(pos.dy / _gridSize).round() * _gridSize,
);
}
/// 在指定位置查找元件
Component? _findComponentAtPosition(Offset worldPos) {
// 从后往前查找(选中上层的元件)
final components = widget.design.components.values.toList().reversed;
for (final component in components) {
// 简化碰撞检测:使用边界框
const componentSize = 40.0; // 假设元件大小
final posX = component.position.x.toDouble();
final posY = component.position.y.toDouble();
if (worldPos.dx >= posX - componentSize / 2 &&
worldPos.dx <= posX + componentSize / 2 &&
worldPos.dy >= posY - componentSize / 2 &&
worldPos.dy <= posY + componentSize / 2) {
return component;
}
}
return null;
}
/// 在指定位置查找引脚
({Component component, PinReference pin})? _findPinAtPosition(Offset worldPos) {
for (final component in widget.design.components.values) {
for (final pin in component.pins) {
final pinX = (component.position.x + pin.x).toDouble();
final pinY = (component.position.y + pin.y).toDouble();
final distance = math.sqrt(
math.pow(worldPos.dx - pinX, 2) + math.pow(worldPos.dy - pinY, 2),
);
if (distance < _snapDistance) {
return (component: component, pin: pin);
}
}
}
return null;
}
/// 显示上下文菜单
void _showContextMenu() {
showModalBottomSheet(
context: context,
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.add),
title: const Text('添加元件'),
onTap: () {
Navigator.pop(context);
// TODO: 添加元件
},
),
if (widget.selectionManager.hasSelection) ...[
ListTile(
leading: const Icon(Icons.rotate_right),
title: const Text('旋转 90°'),
onTap: () {
Navigator.pop(context);
for (final id in widget.selectionManager.getSelectedComponentIds()) {
rotateComponent(id, angle: 90);
}
},
),
ListTile(
leading: const Icon(Icons.delete),
title: const Text('删除'),
onTap: () {
Navigator.pop(context);
_deleteSelected();
},
),
],
],
),
),
);
}
/// 删除选中的对象
void _deleteSelected() {
final componentIds = widget.selectionManager.getSelectedComponentIds();
if (componentIds.isEmpty) return;
final newComponents = Map.from(widget.design.components);
for (final id in componentIds) {
newComponents.remove(id);
}
final newDesign = widget.design.copyWith(
components: newComponents,
updatedAt: DateTime.now().millisecondsSinceEpoch,
isDirty: true,
);
widget.onDesignChanged(newDesign);
widget.selectionManager.clearSelection();
}
}
/// 原理图画布绘制器
class SchematicCanvasPainter extends CustomPainter {
final Design design;
final double zoomLevel;
final Offset offset;
final SelectionManager selectionManager;
final WiringState? wiringState;
final Rect? selectionRect;
final PinReference? hoveredPin;
final Component? hoveredComponent;
final double gridSize;
SchematicCanvasPainter({
required this.design,
required this.zoomLevel,
required this.offset,
required this.selectionManager,
this.wiringState,
this.selectionRect,
this.hoveredPin,
this.hoveredComponent,
required this.gridSize,
});
@override
void paint(Canvas canvas, Size size) {
// 保存画布状态
canvas.save();
// 应用变换
canvas.translate(offset.dx, offset.dy);
canvas.scale(zoomLevel);
// 绘制网格
_drawGrid(canvas, size);
// 绘制网络(走线)
_drawNets(canvas);
// 绘制元件
_drawComponents(canvas);
// 绘制连线中的线
if (wiringState != null) {
_drawWiringLine(canvas);
}
// 绘制框选矩形
if (selectionRect != null) {
_drawSelectionRect(canvas);
}
// 恢复画布状态
canvas.restore();
}
void _drawGrid(Canvas canvas, Size size) {
final paint = Paint()
..color = const Color(0xFFE0E0E0)
..strokeWidth = 0.5;
// 计算可见区域
final startX = (-offset.dx / zoomLevel);
final startY = (-offset.dy / zoomLevel);
final endX = startX + size.width / zoomLevel;
final endY = startY + size.height / zoomLevel;
// 绘制垂直线
for (double x = (startX / gridSize).floor() * gridSize; x < endX; x += gridSize) {
canvas.drawLine(
Offset(x, startY),
Offset(x, endY),
paint,
);
}
// 绘制水平线
for (double y = (startY / gridSize).floor() * gridSize; y < endY; y += gridSize) {
canvas.drawLine(
Offset(startX, y),
Offset(endX, y),
paint,
);
}
}
void _drawComponents(Canvas canvas) {
for (final component in design.components.values) {
final isSelected = selectionManager.isSelectedById(
SelectableType.component,
component.id,
);
final isHovered = hoveredComponent?.id == component.id;
_drawComponent(canvas, component, isSelected: isSelected, isHovered: isHovered);
}
}
void _drawComponent(Canvas canvas, Component component, {bool isSelected = false, bool isHovered = false}) {
final paint = Paint()
..color = isSelected ? const Color(0xFF2196F3) : (isHovered ? const Color(0xFF64B5F6) : const Color(0xFF333333))
..style = PaintingStyle.stroke
..strokeWidth = isSelected ? 3.0 : 2.0;
final fillPaint = Paint()
..color = (isSelected ? const Color(0xFF2196F3) : const Color(0xFFFFFFFF)).withOpacity(0.1)
..style = PaintingStyle.fill;
// 绘制元件主体(矩形)
const size = 40.0;
final rect = Rect.fromLTWH(
component.position.x.toDouble() - size / 2,
component.position.y.toDouble() - size / 2,
size,
size,
);
canvas.drawRect(rect, fillPaint);
canvas.drawRect(rect, paint);
// 绘制元件名称
final textPainter = TextPainter(
text: TextSpan(
text: component.name,
style: const TextStyle(
color: Color(0xFF333333),
fontSize: 12.0,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(
component.position.x.toDouble() - textPainter.width / 2,
component.position.y.toDouble() - size / 2 - 20,
),
);
// 绘制引脚
for (final pin in component.pins) {
_drawPin(canvas, component, pin);
}
}
void _drawPin(Canvas canvas, Component component, PinReference pin) {
final paint = Paint()
..color = const Color(0xFF333333)
..style = PaintingStyle.fill;
final pinX = (component.position.x + pin.x).toDouble();
final pinY = (component.position.y + pin.y).toDouble();
// 绘制引脚(小圆圈)
canvas.drawCircle(Offset(pinX, pinY), 4.0, paint);
// 绘制引脚名称
final textPainter = TextPainter(
text: TextSpan(
text: pin.name,
style: const TextStyle(
color: Color(0xFF666666),
fontSize: 10.0,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(pinX + 6, pinY - 6),
);
}
void _drawNets(Canvas canvas) {
final paint = Paint()
..color = const Color(0xFF4CAF50)
..strokeWidth = 2.0
..style = PaintingStyle.stroke;
for (final net in design.nets.values) {
_drawNet(canvas, net, paint);
}
}
void _drawNet(Canvas canvas, Net net, Paint paint) {
if (net.connections.length < 2) return;
// 简单连接:从第一个点到最后一个点
for (int i = 0; i < net.connections.length - 1; i++) {
final start = net.connections[i];
final end = net.connections[i + 1];
if (start.position != null && end.position != null) {
canvas.drawLine(
Offset(start.position!.x.toDouble(), start.position!.y.toDouble()),
Offset(end.position!.x.toDouble(), end.position!.y.toDouble()),
paint,
);
}
}
}
void _drawWiringLine(Canvas canvas) {
if (wiringState == null) return;
final paint = Paint()
..color = const Color(0xFFFF9800)
..strokeWidth = 2.0
..style = PaintingStyle.stroke;
final start = wiringState!.startPoint;
if (start.position != null) {
canvas.drawLine(
Offset(start.position!.x.toDouble(), start.position!.y.toDouble()),
wiringState!.currentPoint,
paint,
);
}
}
void _drawSelectionRect(Canvas canvas) {
if (selectionRect == null) return;
final paint = Paint()
..color = const Color(0xFF2196F3).withOpacity(0.2)
..style = PaintingStyle.fill;
final borderPaint = Paint()
..color = const Color(0xFF2196F3)
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
canvas.drawRect(
Rect.fromLTRB(
selectionRect!.left,
selectionRect!.top,
selectionRect!.right,
selectionRect!.bottom,
),
paint,
);
canvas.drawRect(
Rect.fromLTRB(
selectionRect!.left,
selectionRect!.top,
selectionRect!.right,
selectionRect!.bottom,
),
borderPaint,
);
}
@override
bool shouldRepaint(SchematicCanvasPainter oldDelegate) {
return oldDelegate.zoomLevel != zoomLevel ||
oldDelegate.offset != offset ||
oldDelegate.selectionManager.selectedObjects != selectionManager.selectedObjects ||
oldDelegate.wiringState != wiringState ||
oldDelegate.selectionRect != selectionRect ||
oldDelegate.hoveredPin != hoveredPin ||
oldDelegate.hoveredComponent != hoveredComponent;
}
}