/** * 可编辑画布组件 * * 基于 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 createState() => _EditableCanvasState(); } class _EditableCanvasState extends State { // 画布状态 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; } }