- Phase 1: Architecture (Flutter) + Data Models + UX Specs - Phase 2: Editable Canvas + UI Components + Import/Export - Phase 3: DRC Engine + Cloud Sync + i18n (4 languages) + Dark Mode - Phase 4: Performance Optimization + Deployment Guides + Test Suite Known P0 issues (to be fixed): - Save functionality not implemented - Component placement not implemented - Canvas rendering incomplete
822 lines
23 KiB
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;
|
|
}
|
|
}
|