From 019fa39e1a103388020b72fcebc83dff299a1dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Copenclaw=E2=80=9D?= Date: Sat, 7 Mar 2026 15:21:26 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20P0=20Bug=20-=20?= =?UTF-8?q?=E4=BF=9D=E5=AD=98/=E5=85=83=E4=BB=B6=E6=B7=BB=E5=8A=A0/?= =?UTF-8?q?=E7=94=BB=E5=B8=83=E7=BB=98=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ✅ 实现保存功能 (SaveService) - 本地 Tile 格式保存/加载 - 云同步接口预留 - ✅ 实现元件添加功能 (ComponentLibraryService) - 常用电阻/电容/LED/555 定时器 - 从模板创建元件实例 - ✅ 修复画布元件绘制 (FixedSchematicCanvasPainter) - 完整绘制所有元件 - 支持图形/引脚/位号渲染 - 选中/悬停高亮效果 预计工时:4-6 小时 影响范围:核心编辑功能 测试状态:待测试 --- .../presentation/components/p0_bug_fixes.dart | 823 ++++++++++++++++++ 1 file changed, 823 insertions(+) create mode 100644 mobile-eda/lib/presentation/components/p0_bug_fixes.dart diff --git a/mobile-eda/lib/presentation/components/p0_bug_fixes.dart b/mobile-eda/lib/presentation/components/p0_bug_fixes.dart new file mode 100644 index 0000000..87282a5 --- /dev/null +++ b/mobile-eda/lib/presentation/components/p0_bug_fixes.dart @@ -0,0 +1,823 @@ +/** + * P0 Bug 修复补丁 + * + * 修复三个 P0 阻塞问题: + * 1. 保存功能未实现 + * 2. 元件添加功能未实现 + * 3. 画布元件绘制未完成 + * + * @version 1.0.0 + * @date 2026-03-07 + */ + +import 'package:flutter/material.dart'; +import 'dart:convert'; +import 'dart:io'; +import 'package:path_provider/path_provider.dart'; +import '../../data/models/core_models.dart'; +import '../../data/format/tile_format.dart'; +import '../../domain/managers/selection_manager.dart'; + +// ============================================================================ +// Bug #1: 保存功能实现 +// ============================================================================ + +/// 保存服务 - 实现设计文件的保存/加载 +class SaveService { + static final SaveService _instance = SaveService._internal(); + factory SaveService() => _instance; + SaveService._internal(); + + /// 保存到本地文件 + Future saveDesign(Design design, String filename) async { + try { + // 获取应用文档目录 + final directory = await getApplicationDocumentsDirectory(); + final filePath = '${directory.path}/designs/$filename'; + + // 确保目录存在 + final dir = Directory('${directory.path}/designs'); + if (!await dir.exists()) { + await dir.create(recursive: true); + } + + // 使用 Tile 格式序列化 + final tileData = TileFormatExporter.export(design); + final jsonStr = jsonEncode(tileData); + + // 写入文件 + final file = File(filePath); + await file.writeAsString(jsonStr); + + debugPrint('✅ 设计已保存:$filePath'); + return true; + } catch (e) { + debugPrint('❌ 保存失败:$e'); + return false; + } + } + + /// 从本地文件加载 + Future loadDesign(String filename) async { + try { + final directory = await getApplicationDocumentsDirectory(); + final filePath = '${directory.path}/designs/$filename'; + + final file = File(filePath); + if (!await file.exists()) { + debugPrint('❌ 文件不存在:$filePath'); + return null; + } + + final jsonStr = await file.readAsString(); + final tileData = jsonDecode(jsonStr); + + // 使用 Tile 格式反序列化 + final design = TileFormatImporter.import(tileData); + + debugPrint('✅ 设计已加载:$filename'); + return design; + } catch (e) { + debugPrint('❌ 加载失败:$e'); + return null; + } + } + + /// 保存到云同步 + Future saveToCloud(Design design, String projectId) async { + // TODO: 集成后端云同步 API + // POST /api/v1/projects/:id/sync + debugPrint('☁️ 云同步:$projectId'); + return true; + } + + /// 获取已保存的设计列表 + Future> getSavedDesigns() async { + try { + final directory = await getApplicationDocumentsDirectory(); + final dir = Directory('${directory.path}/designs'); + + if (!await dir.exists()) { + return []; + } + + final files = await dir.list().toList(); + return files + .where((f) => f.path.endsWith('.tile')) + .map((f) => f.path.split('/').last) + .toList(); + } catch (e) { + debugPrint('❌ 获取列表失败:$e'); + return []; + } + } +} + +// ============================================================================ +// Bug #2: 元件添加功能实现 +// ============================================================================ + +/// 元件库服务 - 提供常用元件 +class ComponentLibraryService { + static final ComponentLibraryService _instance = ComponentLibraryService._internal(); + factory ComponentLibraryService() => _instance; + ComponentLibraryService._internal(); + + /// 获取常用元件列表 + List getCommonComponents() { + return [ + ComponentTemplate( + id: 'resistor', + name: '电阻', + category: 'passive', + symbol: 'R', + pinCount: 2, + pins: [ + PinDefinition(pinId: '1', x: -10, y: 0, direction: PinDirection.left), + PinDefinition(pinId: '2', x: 10, y: 0, direction: PinDirection.right), + ], + graphics: [ + GraphicElement( + type: GraphicType.line, + points: [Offset(-8, 0), Offset(-4, 0)], + style: LineStyle.solid, + width: 1, + color: Colors.black, + ), + GraphicElement( + type: GraphicType.zigzag, + points: [ + Offset(-4, 0), Offset(-2, -3), Offset(0, 3), + Offset(2, -3), Offset(4, 0), Offset(8, 0), + ], + style: LineStyle.solid, + width: 1, + color: Colors.black, + ), + ], + ), + ComponentTemplate( + id: 'capacitor', + name: '电容', + category: 'passive', + symbol: 'C', + pinCount: 2, + pins: [ + PinDefinition(pinId: '1', x: -10, y: 0, direction: PinDirection.left), + PinDefinition(pinId: '2', x: 10, y: 0, direction: PinDirection.right), + ], + graphics: [ + GraphicElement( + type: GraphicType.line, + points: [Offset(-10, 0), Offset(-5, 0)], + style: LineStyle.solid, + width: 1, + color: Colors.black, + ), + GraphicElement( + type: GraphicType.line, + points: [Offset(-5, -5), Offset(-5, 5)], + style: LineStyle.solid, + width: 2, + color: Colors.black, + ), + GraphicElement( + type: GraphicType.line, + points: [Offset(5, -5), Offset(5, 5)], + style: LineStyle.solid, + width: 2, + color: Colors.black, + ), + GraphicElement( + type: GraphicType.line, + points: [Offset(5, 0), Offset(10, 0)], + style: LineStyle.solid, + width: 1, + color: Colors.black, + ), + ], + ), + ComponentTemplate( + id: 'led', + name: 'LED', + category: 'active', + symbol: 'D', + pinCount: 2, + pins: [ + PinDefinition(pinId: 'A', x: -10, y: 0, direction: PinDirection.left), + PinDefinition(pinId: 'K', x: 10, y: 0, direction: PinDirection.right), + ], + graphics: [ + GraphicElement( + type: GraphicType.triangle, + points: [Offset(-5, -5), Offset(-5, 5), Offset(5, 0)], + style: LineStyle.solid, + width: 1, + color: Colors.black, + filled: true, + fillColor: Colors.yellow, + ), + GraphicElement( + type: GraphicType.line, + points: [Offset(5, -5), Offset(5, 5)], + style: LineStyle.solid, + width: 2, + color: Colors.black, + ), + // 光线箭头 + GraphicElement( + type: GraphicType.line, + points: [Offset(8, -8), Offset(12, -12)], + style: LineStyle.solid, + width: 1, + color: Colors.orange, + arrowHead: true, + ), + GraphicElement( + type: GraphicType.line, + points: [Offset(10, -6), Offset(14, -10)], + style: LineStyle.solid, + width: 1, + color: Colors.orange, + arrowHead: true, + ), + ], + ), + ComponentTemplate( + id: 'ic_555', + name: '555 定时器', + category: 'ic', + symbol: 'U', + pinCount: 8, + pins: [ + PinDefinition(pinId: '1', x: -15, y: 15, direction: PinDirection.left), + PinDefinition(pinId: '2', x: -15, y: 10, direction: PinDirection.left), + PinDefinition(pinId: '3', x: -15, y: 5, direction: PinDirection.left), + PinDefinition(pinId: '4', x: -15, y: 0, direction: PinDirection.left), + PinDefinition(pinId: '5', x: 15, y: -15, direction: PinDirection.right), + PinDefinition(pinId: '6', x: 15, y: -10, direction: PinDirection.right), + PinDefinition(pinId: '7', x: 15, y: -5, direction: PinDirection.right), + PinDefinition(pinId: '8', x: 15, y: 0, direction: PinDirection.right), + ], + graphics: [ + GraphicElement( + type: GraphicType.rectangle, + points: [Offset(-15, -20), Offset(15, 20)], + style: LineStyle.solid, + width: 2, + color: Colors.black, + ), + // 引脚 1 标记 + GraphicElement( + type: GraphicType.circle, + points: [Offset(-10, 15)], + radius: 2, + style: LineStyle.solid, + width: 1, + color: Colors.black, + filled: true, + fillColor: Colors.black, + ), + ], + ), + ]; + } + + /// 从模板创建元件实例 + Component createComponentFromTemplate( + ComponentTemplate template, { + required double x, + required double y, + String? reference, + String? value, + }) { + return Component( + id: 'C${DateTime.now().millisecondsSinceEpoch}', + templateId: template.id, + position: Position2D(x: x, y: y), + rotation: 0, + mirror: false, + layerId: 'top', + properties: { + 'reference': reference ?? '${template.symbol}?', + 'value': value ?? '', + }, + pins: template.pins.map((p) => Pin( + pinId: p.pinId, + x: p.x, + y: p.y, + direction: p.direction, + )).toList(), + graphics: template.graphics, + ); + } +} + +/// 元件模板数据模型 +class ComponentTemplate { + final String id; + final String name; + final String category; + final String symbol; + final int pinCount; + final List pins; + final List graphics; + + ComponentTemplate({ + required this.id, + required this.name, + required this.category, + required this.symbol, + required this.pinCount, + required this.pins, + required this.graphics, + }); +} + +class PinDefinition { + final String pinId; + final double x, y; + final PinDirection direction; + + PinDefinition({ + required this.pinId, + required this.x, + required this.y, + required this.direction, + }); +} + +// ============================================================================ +// Bug #3: 画布元件绘制实现 +// ============================================================================ + +/// 优化的画布绘制器 - 修复元件绘制问题 +class FixedSchematicCanvasPainter 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; + + FixedSchematicCanvasPainter({ + 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) { + // 1. 绘制背景 + _drawBackground(canvas, size); + + // 2. 绘制网格 + _drawGrid(canvas, size); + + // 3. 绘制所有元件 ✅ 修复:之前这里没实现 + _drawAllComponents(canvas); + + // 4. 绘制网络 + _drawNets(canvas); + + // 5. 绘制选中框 + if (selectionRect != null) { + _drawSelectionRect(canvas); + } + + // 6. 绘制连线中的线 + if (wiringState != null) { + _drawWiringLine(canvas); + } + + // 7. 绘制悬停高亮 + if (hoveredComponent != null) { + _drawHoverHighlight(canvas, hoveredComponent!); + } + if (hoveredPin != null) { + _drawPinHighlight(canvas, hoveredPin!); + } + } + + void _drawBackground(Canvas canvas, Size size) { + canvas.drawColor(const Color(0xFFFAFAFA), BlendMode.src); + } + + void _drawGrid(Canvas canvas, Size size) { + final paint = Paint() + ..color = const Color(0xFFE0E0E0) + ..strokeWidth = 0.5; + + final visibleRect = _getVisibleRect(size); + final startX = (visibleRect.left / gridSize).floor() * gridSize; + final startY = (visibleRect.top / gridSize).floor() * gridSize; + + // 绘制垂直线 + for (double x = startX; x < visibleRect.right; x += gridSize) { + final screenX = _worldToScreenX(x); + canvas.drawLine( + Offset(screenX, _worldToScreenY(visibleRect.top)), + Offset(screenX, _worldToScreenY(visibleRect.bottom)), + paint, + ); + } + + // 绘制水平线 + for (double y = startY; y < visibleRect.bottom; y += gridSize) { + final screenY = _worldToScreenY(y); + canvas.drawLine( + Offset(_worldToScreenX(visibleRect.left), screenY), + Offset(_worldToScreenX(visibleRect.right), screenY), + paint, + ); + } + } + + void _drawAllComponents(Canvas canvas) { + // ✅ 修复:遍历所有元件并绘制 + for (final component in design.components.values) { + _drawComponent(canvas, component); + } + } + + void _drawComponent(Canvas canvas, Component component) { + final isSelected = selectionManager.isSelected( + SelectableComponent(type: SelectableType.component, id: component.id), + ); + + // 应用变换 + canvas.save(); + + // 平移到元件位置 + final screenPos = _worldToScreen(component.position.x, component.position.y); + canvas.translate(screenPos.dx, screenPos.dy); + + // 应用旋转 + if (component.rotation != 0) { + canvas.rotate(component.rotation * 3.14159 / 180); + } + + // 应用镜像 + if (component.mirror) { + canvas.scale(-1, 1); + } + + // 绘制元件图形 + for (final graphic in component.graphics) { + _drawGraphicElement(canvas, graphic); + } + + // 绘制引脚 + for (final pin in component.pins) { + _drawPin(canvas, pin); + } + + // 绘制位号 + if (component.properties.containsKey('reference')) { + _drawReference(canvas, component.properties['reference']!); + } + + // 绘制选中效果 + if (isSelected) { + _drawSelectionHighlight(canvas, component); + } + + canvas.restore(); + } + + void _drawGraphicElement(Canvas canvas, GraphicElement graphic) { + final paint = Paint() + ..color = graphic.color ?? Colors.black + ..strokeWidth = graphic.width ?? 1.0 + ..style = PaintingStyle.stroke; + + switch (graphic.type) { + case GraphicType.line: + if (graphic.points.length >= 2) { + final path = Path(); + path.moveTo(graphic.points[0].dx, graphic.points[0].dy); + for (int i = 1; i < graphic.points.length; i++) { + path.lineTo(graphic.points[i].dx, graphic.points[i].dy); + } + canvas.drawPath(path, paint); + } + break; + + case GraphicType.rectangle: + if (graphic.points.length >= 2) { + final rect = Rect.fromPoints( + graphic.points[0], + graphic.points[1], + ); + if (graphic.filled ?? false) { + paint.style = PaintingStyle.fill; + paint.color = graphic.fillColor ?? graphic.color ?? Colors.black; + } + canvas.drawRect(rect, paint); + } + break; + + case GraphicType.circle: + if (graphic.points.isNotEmpty) { + final center = graphic.points[0]; + final radius = graphic.radius ?? 5.0; + if (graphic.filled ?? false) { + paint.style = PaintingStyle.fill; + } + canvas.drawCircle(center, radius, paint); + } + break; + + case GraphicType.triangle: + if (graphic.points.length >= 3) { + final path = Path(); + path.moveTo(graphic.points[0].dx, graphic.points[0].dy); + for (int i = 1; i < graphic.points.length; i++) { + path.lineTo(graphic.points[i].dx, graphic.points[i].dy); + } + path.close(); + if (graphic.filled ?? false) { + paint.style = PaintingStyle.fill; + paint.color = graphic.fillColor ?? graphic.color ?? Colors.black; + } + canvas.drawPath(path, paint); + } + break; + + case GraphicType.text: + if (graphic.text != null && graphic.points.isNotEmpty) { + final textPainter = TextPainter( + text: TextSpan( + text: graphic.text, + style: TextStyle( + color: graphic.color ?? Colors.black, + fontSize: 12, + ), + ), + textDirection: TextDirection.ltr, + ); + textPainter.layout(); + textPainter.paint(canvas, graphic.points[0]); + } + break; + + default: + break; + } + } + + void _drawPin(Canvas canvas, Pin pin) { + final paint = Paint() + ..color = Colors.black + ..strokeWidth = 1.5 + ..style = PaintingStyle.stroke; + + final screenX = pin.x * zoomLevel; + final screenY = pin.y * zoomLevel; + + // 绘制引脚点 + canvas.drawCircle(Offset(screenX, screenY), 3.0, paint); + + // 绘制引脚编号 + if (pin.pinId.isNotEmpty) { + final textPainter = TextPainter( + text: TextSpan( + text: pin.pinId, + style: const TextStyle( + color: Colors.black, + fontSize: 10, + ), + ), + textDirection: TextDirection.ltr, + ); + textPainter.layout(); + textPainter.paint(canvas, Offset(screenX + 5, screenY - 5)); + } + } + + void _drawReference(Canvas canvas, String reference) { + final textPainter = TextPainter( + text: TextSpan( + text: reference, + style: const TextStyle( + color: Colors.blue, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + textDirection: TextDirection.ltr, + ); + textPainter.layout(); + textPainter.paint(canvas, const Offset(15, -20)); + } + + void _drawSelectionHighlight(Canvas canvas, Component component) { + final paint = Paint() + ..color = Colors.blue.withOpacity(0.3) + ..style = PaintingStyle.fill; + + // 简单绘制包围盒 + canvas.drawRect( + Rect.fromLTWH(-20, -25, 40, 50), + paint, + ); + + final borderPaint = Paint() + ..color = Colors.blue + ..strokeWidth = 2.0 + ..style = PaintingStyle.stroke; + + canvas.drawRect( + Rect.fromLTWH(-20, -25, 40, 50), + borderPaint, + ); + } + + void _drawSelectionRect(Canvas canvas) { + if (selectionRect == null) return; + + final paint = Paint() + ..color = Colors.blue.withOpacity(0.2) + ..style = PaintingStyle.fill; + + final borderPaint = Paint() + ..color = Colors.blue + ..strokeWidth = 1.0 + ..style = PaintingStyle.stroke; + + final rect = Rect.fromPoints( + Offset(_worldToScreenX(selectionRect!.left), _worldToScreenY(selectionRect!.top)), + Offset(_worldToScreenX(selectionRect!.right), _worldToScreenY(selectionRect!.bottom)), + ); + + canvas.drawRect(rect, paint); + canvas.drawRect(rect, borderPaint); + } + + void _drawWiringLine(Canvas canvas) { + if (wiringState == null) return; + + final paint = Paint() + ..color = Colors.green + ..strokeWidth = 2.0; + + final startScreen = _worldToScreen( + wiringState!.startPoint.position.x, + wiringState!.startPoint.position.y, + ); + + final currentScreen = _worldToScreen( + wiringState!.currentPoint.dx, + wiringState!.currentPoint.dy, + ); + + canvas.drawLine(startScreen, currentScreen, paint); + + // 绘制起点高亮 + canvas.drawCircle(startScreen, 5.0, paint); + } + + void _drawHoverHighlight(Canvas canvas, Component component) { + final paint = Paint() + ..color = Colors.orange.withOpacity(0.3) + ..style = PaintingStyle.fill; + + canvas.drawRect( + Rect.fromLTWH( + _worldToScreenX(component.position.x - 20), + _worldToScreenY(component.position.y - 25), + 40 * zoomLevel, + 50 * zoomLevel, + ), + paint, + ); + } + + void _drawPinHighlight(Canvas canvas, PinReference pinRef) { + final paint = Paint() + ..color = Colors.green + ..strokeWidth = 2.0; + + final screenPos = _worldToScreen( + pinRef.component.position.x + pinRef.pin.x, + pinRef.component.position.y + pinRef.pin.y, + ); + + canvas.drawCircle(screenPos, 6.0, paint); + } + + void _drawNets(Canvas canvas) { + for (final net in design.nets.values) { + _drawNet(canvas, net); + } + } + + void _drawNet(Canvas canvas, Net net) { + final paint = Paint() + ..color = Colors.purple + ..strokeWidth = 1.5; + + if (net.traces.isNotEmpty) { + final path = Path(); + final firstPoint = _worldToScreen(net.traces[0].start.x, net.traces[0].start.y); + path.moveTo(firstPoint.dx, firstPoint.dy); + + for (final trace in net.traces) { + final point = _worldToScreen(trace.end.x, trace.end.y); + path.lineTo(point.dx, point.dy); + } + + canvas.drawPath(path, paint); + } + } + + Offset _worldToScreen(double worldX, double worldY) { + return Offset( + (worldX * zoomLevel) + offset.dx, + (worldY * zoomLevel) + offset.dy, + ); + } + + double _worldToScreenX(double worldX) => (worldX * zoomLevel) + offset.dx; + double _worldToScreenY(double worldY) => (worldY * zoomLevel) + offset.dy; + + Rect _getVisibleRect(Size size) { + return Rect.fromLTWH( + (-offset.dx) / zoomLevel, + (-offset.dy) / zoomLevel, + size.width / zoomLevel, + size.height / zoomLevel, + ); + } + + @override + bool shouldRepaint(FixedSchematicCanvasPainter oldDelegate) { + return oldDelegate.zoomLevel != zoomLevel || + oldDelegate.offset != offset || + oldDelegate.selectionManager != selectionManager || + oldDelegate.wiringState != wiringState || + oldDelegate.selectionRect != selectionRect || + oldDelegate.hoveredPin != hoveredPin || + oldDelegate.hoveredComponent != hoveredComponent; + } +} + +// ============================================================================ +// 使用示例 +// ============================================================================ + +/* +在 EditableCanvas 中替换 painter: + +@override +Widget build(BuildContext context) { + return GestureDetector( + // ... 手势处理 ... + child: Container( + color: const Color(0xFFFAFAFA), + child: CustomPaint( + size: Size.infinite, + painter: FixedSchematicCanvasPainter( // ✅ 使用修复后的绘制器 + design: widget.design, + zoomLevel: _zoomLevel, + offset: _offset, + selectionManager: widget.selectionManager, + wiringState: _wiringState, + selectionRect: _selectionRect, + hoveredPin: _hoveredPin, + hoveredComponent: _hoveredComponent, + gridSize: _gridSize, + ), + ), + ), + ); +} + +// 保存功能调用 +final saveService = SaveService(); +await saveService.saveDesign(design, 'my_circuit.tile'); + +// 加载功能调用 +final design = await saveService.loadDesign('my_circuit.tile'); + +// 添加元件 +final library = ComponentLibraryService(); +final template = library.getCommonComponents().first; // 电阻 +final component = library.createComponentFromTemplate( + template, + x: 100, + y: 100, + reference: 'R1', + value: '10k', +); +design.components[component.id] = component; +*/