- 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
755 lines
20 KiB
Dart
755 lines
20 KiB
Dart
/// KiCad 原理图文件导入器
|
|
///
|
|
/// 解析 KiCad .sch 和 .kicad_sch 文件,映射到核心数据模型
|
|
///
|
|
/// @version 0.1.0
|
|
/// @date 2026-03-07
|
|
|
|
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
// ============================================================================
|
|
// KiCad S-表达式解析器
|
|
// ============================================================================
|
|
|
|
/// KiCad S-表达式词法单元
|
|
enum SExprToken {
|
|
leftParen,
|
|
rightParen,
|
|
string,
|
|
number,
|
|
symbol,
|
|
eof,
|
|
}
|
|
|
|
/// S-表达式解析器
|
|
class SExprParser {
|
|
final String input;
|
|
int position = 0;
|
|
|
|
SExprParser(this.input);
|
|
|
|
/// 解析整个 S-表达式
|
|
dynamic parse() {
|
|
_skipWhitespace();
|
|
if (position >= input.length) {
|
|
return null;
|
|
}
|
|
return _parseExpr();
|
|
}
|
|
|
|
dynamic _parseExpr() {
|
|
_skipWhitespace();
|
|
|
|
if (position >= input.length) {
|
|
return null;
|
|
}
|
|
|
|
final char = input[position];
|
|
|
|
if (char == '(') {
|
|
return _parseList();
|
|
} else if (char == '"' || char == '\'') {
|
|
return _parseString();
|
|
} else if (char == '-' || (char >= '0' && char <= '9')) {
|
|
return _parseNumber();
|
|
} else {
|
|
return _parseSymbol();
|
|
}
|
|
}
|
|
|
|
List<dynamic> _parseList() {
|
|
position++; // skip '('
|
|
final list = <dynamic>[];
|
|
|
|
while (true) {
|
|
_skipWhitespace();
|
|
|
|
if (position >= input.length) {
|
|
throw FormatException('Unexpected end of input in list');
|
|
}
|
|
|
|
if (input[position] == ')') {
|
|
position++; // skip ')'
|
|
break;
|
|
}
|
|
|
|
list.add(_parseExpr());
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
String _parseString() {
|
|
final quote = input[position];
|
|
position++; // skip opening quote
|
|
|
|
final buffer = StringBuffer();
|
|
|
|
while (position < input.length) {
|
|
final char = input[position];
|
|
|
|
if (char == quote) {
|
|
position++; // skip closing quote
|
|
break;
|
|
}
|
|
|
|
if (char == '\\' && position + 1 < input.length) {
|
|
position++;
|
|
final escaped = input[position];
|
|
switch (escaped) {
|
|
case 'n': buffer.write('\n'); break;
|
|
case 't': buffer.write('\t'); break;
|
|
case 'r': buffer.write('\r'); break;
|
|
case '\\': buffer.write('\\'); break;
|
|
case '"': buffer.write('"'); break;
|
|
default: buffer.write(escaped);
|
|
}
|
|
} else {
|
|
buffer.write(char);
|
|
}
|
|
|
|
position++;
|
|
}
|
|
|
|
return buffer.toString();
|
|
}
|
|
|
|
num _parseNumber() {
|
|
final start = position;
|
|
|
|
if (input[position] == '-') {
|
|
position++;
|
|
}
|
|
|
|
while (position < input.length &&
|
|
(input[position] >= '0' && input[position] <= '9')) {
|
|
position++;
|
|
}
|
|
|
|
if (position < input.length && input[position] == '.') {
|
|
position++;
|
|
while (position < input.length &&
|
|
(input[position] >= '0' && input[position] <= '9')) {
|
|
position++;
|
|
}
|
|
return double.parse(input.substring(start, position));
|
|
}
|
|
|
|
return int.parse(input.substring(start, position));
|
|
}
|
|
|
|
String _parseSymbol() {
|
|
final start = position;
|
|
|
|
while (position < input.length &&
|
|
!_isWhitespace(input[position]) &&
|
|
input[position] != '(' &&
|
|
input[position] != ')' &&
|
|
input[position] != '"' &&
|
|
input[position] != '\'') {
|
|
position++;
|
|
}
|
|
|
|
return input.substring(start, position);
|
|
}
|
|
|
|
void _skipWhitespace() {
|
|
while (position < input.length && _isWhitespace(input[position])) {
|
|
position++;
|
|
}
|
|
}
|
|
|
|
bool _isWhitespace(String char) {
|
|
return char == ' ' || char == '\t' || char == '\n' || char == '\r';
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// KiCad 数据模型
|
|
// ============================================================================
|
|
|
|
/// KiCad 元件
|
|
class KicadComponent {
|
|
final String reference;
|
|
final String value;
|
|
final String footprint;
|
|
final String? libraryLink;
|
|
final double x;
|
|
final double y;
|
|
final int rotation;
|
|
final List<KicadPin> pins;
|
|
final Map<String, String> properties;
|
|
|
|
KicadComponent({
|
|
required this.reference,
|
|
required this.value,
|
|
required this.footprint,
|
|
this.libraryLink,
|
|
required this.x,
|
|
required this.y,
|
|
this.rotation = 0,
|
|
this.pins = const [],
|
|
this.properties = const {},
|
|
});
|
|
|
|
factory KicadComponent.fromSExpr(List<dynamic> expr) {
|
|
String reference = '';
|
|
String value = '';
|
|
String footprint = '';
|
|
String? libraryLink;
|
|
double x = 0;
|
|
double y = 0;
|
|
int rotation = 0;
|
|
final pins = <KicadPin>[];
|
|
final properties = <String, String>{};
|
|
|
|
for (int i = 1; i < expr.length; i++) {
|
|
final item = expr[i];
|
|
if (item is! List) continue;
|
|
|
|
if (item.isNotEmpty && item[0] == 'property') {
|
|
final propName = item.length > 1 ? item[1].toString() : '';
|
|
final propValue = item.length > 2 ? item[2].toString() : '';
|
|
|
|
if (propName == 'Reference') {
|
|
reference = propValue;
|
|
} else if (propName == 'Value') {
|
|
value = propValue;
|
|
} else {
|
|
properties[propName] = propValue;
|
|
}
|
|
} else if (item.isNotEmpty && item[0] == 'symbol') {
|
|
libraryLink = item.length > 1 ? item[1].toString() : null;
|
|
} else if (item.isNotEmpty && item[0] == 'footprint') {
|
|
footprint = item.length > 1 ? item[1].toString() : '';
|
|
} else if (item.isNotEmpty && item[0] == 'at') {
|
|
if (item.length >= 3) {
|
|
x = _parseNumber(item[1]);
|
|
y = _parseNumber(item[2]);
|
|
if (item.length >= 4) {
|
|
rotation = _parseNumber(item[3]).toInt();
|
|
}
|
|
}
|
|
} else if (item.isNotEmpty && item[0] == 'pin') {
|
|
pins.add(KicadPin.fromSExpr(item));
|
|
}
|
|
}
|
|
|
|
return KicadComponent(
|
|
reference: reference,
|
|
value: value,
|
|
footprint: footprint,
|
|
libraryLink: libraryLink,
|
|
x: x,
|
|
y: y,
|
|
rotation: rotation,
|
|
pins: pins,
|
|
properties: properties,
|
|
);
|
|
}
|
|
|
|
static num _parseNumber(dynamic value) {
|
|
if (value is num) return value;
|
|
if (value is String) {
|
|
return double.tryParse(value) ?? 0;
|
|
}
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/// KiCad 引脚
|
|
class KicadPin {
|
|
final String number;
|
|
final String name;
|
|
final double x;
|
|
final double y;
|
|
final int rotation;
|
|
final String type;
|
|
|
|
KicadPin({
|
|
required this.number,
|
|
required this.name,
|
|
required this.x,
|
|
required this.y,
|
|
this.rotation = 0,
|
|
this.type = 'passive',
|
|
});
|
|
|
|
factory KicadPin.fromSExpr(List<dynamic> expr) {
|
|
String number = '';
|
|
String name = '';
|
|
double x = 0;
|
|
double y = 0;
|
|
int rotation = 0;
|
|
String type = 'passive';
|
|
|
|
for (int i = 1; i < expr.length; i++) {
|
|
final item = expr[i];
|
|
|
|
if (item is List) {
|
|
if (item.isNotEmpty && item[0] == 'at') {
|
|
if (item.length >= 3) {
|
|
x = _parseNumber(item[1]);
|
|
y = _parseNumber(item[2]);
|
|
if (item.length >= 4) {
|
|
rotation = _parseNumber(item[3]).toInt();
|
|
}
|
|
}
|
|
} else if (item.isNotEmpty && item[0] == 'property') {
|
|
if (item.length >= 3) {
|
|
final propName = item[1].toString();
|
|
final propValue = item[2].toString();
|
|
if (propName == 'Name') {
|
|
name = propValue;
|
|
} else if (propName == 'Number') {
|
|
number = propValue;
|
|
}
|
|
}
|
|
}
|
|
} else if (item is String) {
|
|
if (item == 'input' || item == 'output' || item == 'bidirectional' ||
|
|
item == 'power_in' || item == 'power_out' || item == 'passive') {
|
|
type = item;
|
|
}
|
|
}
|
|
}
|
|
|
|
return KicadPin(
|
|
number: number,
|
|
name: name,
|
|
x: x,
|
|
y: y,
|
|
rotation: rotation,
|
|
type: type,
|
|
);
|
|
}
|
|
|
|
static num _parseNumber(dynamic value) {
|
|
if (value is num) return value;
|
|
if (value is String) {
|
|
return double.tryParse(value) ?? 0;
|
|
}
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/// KiCad 网络
|
|
class KicadNet {
|
|
final String name;
|
|
final List<KicadNetConnection> connections;
|
|
|
|
KicadNet({
|
|
required this.name,
|
|
this.connections = const [],
|
|
});
|
|
|
|
factory KicadNet.fromSExpr(List<dynamic> expr) {
|
|
String name = '';
|
|
final connections = <KicadNetConnection>[];
|
|
|
|
for (int i = 1; i < expr.length; i++) {
|
|
final item = expr[i];
|
|
|
|
if (item is List) {
|
|
if (item.isNotEmpty && item[0] == 'node') {
|
|
connections.add(KicadNetConnection.fromSExpr(item));
|
|
}
|
|
} else if (item is String && i == 1) {
|
|
name = item;
|
|
}
|
|
}
|
|
|
|
return KicadNet(
|
|
name: name,
|
|
connections: connections,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// KiCad 网络连接
|
|
class KicadNetConnection {
|
|
final String? pinNumber;
|
|
final String? componentRef;
|
|
final double? x;
|
|
final double? y;
|
|
|
|
KicadNetConnection({
|
|
this.pinNumber,
|
|
this.componentRef,
|
|
this.x,
|
|
this.y,
|
|
});
|
|
|
|
factory KicadNetConnection.fromSExpr(List<dynamic> expr) {
|
|
String? pinNumber;
|
|
String? componentRef;
|
|
double? x, y;
|
|
|
|
for (int i = 1; i < expr.length; i++) {
|
|
final item = expr[i];
|
|
|
|
if (item is List) {
|
|
if (item.isNotEmpty && item[0] == 'pin') {
|
|
if (item.length >= 2) {
|
|
pinNumber = item[1].toString();
|
|
}
|
|
// 查找引脚所属的元件引用
|
|
for (int j = 2; j < item.length; j++) {
|
|
if (item[j] is List && item[j][0] == 'lib_pin') {
|
|
// 提取元件引用信息
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return KicadNetConnection(
|
|
pinNumber: pinNumber,
|
|
componentRef: componentRef,
|
|
x: x,
|
|
y: y,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// KiCad 原理图
|
|
class KicadSchematic {
|
|
final String version;
|
|
final List<KicadComponent> components;
|
|
final List<KicadNet> nets;
|
|
|
|
KicadSchematic({
|
|
required this.version,
|
|
this.components = const [],
|
|
this.nets = const [],
|
|
});
|
|
|
|
factory KicadSchematic.fromSExpr(List<dynamic> expr) {
|
|
String version = '';
|
|
final components = <KicadComponent>[];
|
|
final nets = <KicadNet>[];
|
|
|
|
for (int i = 1; i < expr.length; i++) {
|
|
final item = expr[i];
|
|
|
|
if (item is! List) continue;
|
|
|
|
if (item.isNotEmpty && item[0] == 'version') {
|
|
version = item.length > 1 ? item[1].toString() : '';
|
|
} else if (item.isNotEmpty && item[0] == 'component') {
|
|
components.add(KicadComponent.fromSExpr(item));
|
|
} else if (item.isNotEmpty && item[0] == 'net') {
|
|
nets.add(KicadNet.fromSExpr(item));
|
|
}
|
|
}
|
|
|
|
return KicadSchematic(
|
|
version: version,
|
|
components: components,
|
|
nets: nets,
|
|
);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// KiCad 导入器
|
|
// ============================================================================
|
|
|
|
/// KiCad 原理图导入器
|
|
class KicadImporter {
|
|
/// 导入 KiCad .kicad_sch 文件内容
|
|
KicadSchematic import(String content) {
|
|
final parser = SExprParser(content);
|
|
final sExpr = parser.parse();
|
|
|
|
if (sExpr is! List) {
|
|
throw FormatException('Invalid KiCad schematic format');
|
|
}
|
|
|
|
return KicadSchematic.fromSExpr(sExpr);
|
|
}
|
|
|
|
/// 导入 KiCad .sch 文件 (旧格式)
|
|
KicadSchematic importLegacySch(String content) {
|
|
// 旧格式解析逻辑 (简化实现)
|
|
final components = <KicadComponent>[];
|
|
final nets = <KicadNet>[];
|
|
|
|
final lines = content.split('\n');
|
|
KicadComponent? currentComponent;
|
|
|
|
for (final line in lines) {
|
|
final trimmed = line.trim();
|
|
|
|
if (trimmed.startsWith('C ')) {
|
|
// 元件定义行
|
|
if (currentComponent != null) {
|
|
components.add(currentComponent);
|
|
}
|
|
currentComponent = _parseLegacyComponent(line);
|
|
} else if (trimmed.startsWith('N ')) {
|
|
// 网络定义
|
|
final net = _parseLegacyNet(line);
|
|
if (net != null) {
|
|
nets.add(net);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (currentComponent != null) {
|
|
components.add(currentComponent);
|
|
}
|
|
|
|
return KicadSchematic(
|
|
version: 'legacy',
|
|
components: components,
|
|
nets: nets,
|
|
);
|
|
}
|
|
|
|
KicadComponent? _parseLegacyComponent(String line) {
|
|
// 简化实现,实际需要完整的旧格式解析
|
|
final parts = line.split(RegExp(r'\s+'));
|
|
if (parts.length < 4) return null;
|
|
|
|
return KicadComponent(
|
|
reference: parts[1],
|
|
value: parts.length > 2 ? parts[2] : '',
|
|
footprint: '',
|
|
x: double.tryParse(parts[3]) ?? 0,
|
|
y: double.tryParse(parts.length > 4 ? parts[4] : '0') ?? 0,
|
|
);
|
|
}
|
|
|
|
KicadNet? _parseLegacyNet(String line) {
|
|
// 简化实现
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 转换为 EDA 核心数据模型
|
|
// ============================================================================
|
|
|
|
/// 将 KiCad 原理图转换为 EDA 核心 Design 模型
|
|
Map<String, dynamic> kicadToDesign(KicadSchematic kicadSch) {
|
|
final design = <String, dynamic>{
|
|
'id': _generateId('design'),
|
|
'name': 'Imported from KiCad',
|
|
'version': '1.0.0',
|
|
'parameters': {
|
|
'boardSize': {'width': 100000000, 'height': 100000000}, // 100mm x 100mm
|
|
'origin': {'x': 0, 'y': 0},
|
|
'units': 'nm',
|
|
'grid': {'x': 25400, 'y': 25400}, // 10mil grid
|
|
},
|
|
'tables': {
|
|
'components': <Map<String, dynamic>>[],
|
|
'nets': <Map<String, dynamic>>[],
|
|
'layers': <Map<String, dynamic>>[],
|
|
'footprints': <Map<String, dynamic>>[],
|
|
'traces': <Map<String, dynamic>>[],
|
|
'vias': <Map<String, dynamic>>[],
|
|
'polygons': <Map<String, dynamic>>[],
|
|
},
|
|
'stackup': {
|
|
'layers': ['top', 'bottom'],
|
|
'totalThickness': 1600000, // 1.6mm
|
|
},
|
|
'designRules': {
|
|
'traceWidth': {'min': 152400, 'max': 2000000, 'preferred': 254000},
|
|
'traceSpacing': {'min': 152400, 'preferred': 203200},
|
|
'viaHoleSize': {'min': 200000, 'max': 800000},
|
|
'viaDiameter': {'min': 400000, 'max': 1000000},
|
|
'clearance': {'componentToComponent': 500000, 'componentToEdge': 1000000},
|
|
},
|
|
'metadata': {
|
|
'createdAt': DateTime.now().millisecondsSinceEpoch,
|
|
'updatedAt': DateTime.now().millisecondsSinceEpoch,
|
|
'isDirty': false,
|
|
},
|
|
};
|
|
|
|
// 添加默认层
|
|
(design['tables']['layers'] as List).addAll([
|
|
_createLayer('top', 'Top', 'signal', 0),
|
|
_createLayer('bottom', 'Bottom', 'signal', 1),
|
|
_createLayer('silkscreen', 'Silkscreen', 'silkscreen', 2),
|
|
]);
|
|
|
|
// 转换元件
|
|
final componentMap = <String, Map<String, dynamic>>{};
|
|
for (final kicadComp in kicadSch.components) {
|
|
final component = _convertComponent(kicadComp);
|
|
componentMap[kicadComp.reference] = component;
|
|
(design['tables']['components'] as List).add(component);
|
|
}
|
|
|
|
// 转换网络
|
|
for (final kicadNet in kicadSch.nets) {
|
|
final net = _convertNet(kicadNet, componentMap);
|
|
(design['tables']['nets'] as List).add(net);
|
|
}
|
|
|
|
return design;
|
|
}
|
|
|
|
Map<String, dynamic> _convertComponent(KicadComponent kicadComp) {
|
|
// 转换 KiCad 单位 (mm) 到纳米
|
|
final xNm = (kicadComp.x * 1000000).toInt();
|
|
final yNm = (kicadComp.y * 1000000).toInt();
|
|
|
|
// 转换引脚
|
|
final pins = <Map<String, dynamic>>[];
|
|
for (final pin in kicadComp.pins) {
|
|
pins.add({
|
|
'pinId': _generateId('pin'),
|
|
'name': pin.name,
|
|
'x': (pin.x * 1000000).toInt(),
|
|
'y': (pin.y * 1000000).toInt(),
|
|
'rotation': pin.rotation,
|
|
'electricalType': _convertPinType(pin.type),
|
|
});
|
|
}
|
|
|
|
// 确定元件类型
|
|
final type = _determineComponentType(kicadComp.value);
|
|
|
|
return {
|
|
'id': _generateId('comp'),
|
|
'name': kicadComp.reference,
|
|
'type': type,
|
|
'value': kicadComp.value,
|
|
'footprint': {
|
|
'id': _generateId('footprint'),
|
|
'name': kicadComp.footprint,
|
|
'pads': [],
|
|
},
|
|
'position': {
|
|
'x': xNm,
|
|
'y': yNm,
|
|
'layer': 'top',
|
|
'rotation': kicadComp.rotation,
|
|
'mirror': 'none',
|
|
},
|
|
'pins': pins,
|
|
'attributes': {
|
|
'manufacturer': kicadComp.properties['Manufacturer'] ?? '',
|
|
'partNumber': kicadComp.properties['PartNumber'] ?? '',
|
|
'description': kicadComp.properties['Description'] ?? '',
|
|
},
|
|
'metadata': {
|
|
'createdAt': DateTime.now().millisecondsSinceEpoch,
|
|
'updatedAt': DateTime.now().millisecondsSinceEpoch,
|
|
},
|
|
};
|
|
}
|
|
|
|
Map<String, dynamic> _convertNet(KicadNet kicadNet, Map<String, Map<String, dynamic>> componentMap) {
|
|
final connections = <Map<String, dynamic>>[];
|
|
|
|
for (final conn in kicadNet.connections) {
|
|
if (conn.componentRef != null && componentMap.containsKey(conn.componentRef)) {
|
|
final component = componentMap[conn.componentRef]!;
|
|
connections.add({
|
|
'id': _generateId('conn'),
|
|
'type': 'pin',
|
|
'componentId': component['id'],
|
|
'pinId': conn.pinNumber,
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
'id': _generateId('net'),
|
|
'name': kicadNet.name,
|
|
'type': 'signal',
|
|
'connections': connections,
|
|
'properties': {},
|
|
'metadata': {
|
|
'createdAt': DateTime.now().millisecondsSinceEpoch,
|
|
'updatedAt': DateTime.now().millisecondsSinceEpoch,
|
|
},
|
|
};
|
|
}
|
|
|
|
Map<String, dynamic> _createLayer(String id, String name, String type, int order) {
|
|
return {
|
|
'id': id,
|
|
'name': name,
|
|
'type': type,
|
|
'stackupOrder': order,
|
|
'properties': {
|
|
'isVisible': true,
|
|
'isEnabled': true,
|
|
'color': type == 'signal' ? '#00ff00' : '#ffffff',
|
|
},
|
|
'objects': {},
|
|
'metadata': {
|
|
'createdAt': DateTime.now().millisecondsSinceEpoch,
|
|
'updatedAt': DateTime.now().millisecondsSinceEpoch,
|
|
},
|
|
};
|
|
}
|
|
|
|
String _determineComponentType(String value) {
|
|
final lowerValue = value.toLowerCase();
|
|
|
|
if (lowerValue.contains('r') || lowerValue.contains('resistor')) {
|
|
return 'resistor';
|
|
} else if (lowerValue.contains('c') || lowerValue.contains('capacitor')) {
|
|
return 'capacitor';
|
|
} else if (lowerValue.contains('l') || lowerValue.contains('inductor')) {
|
|
return 'inductor';
|
|
} else if (lowerValue.contains('d') || lowerValue.contains('diode')) {
|
|
return 'diode';
|
|
} else if (lowerValue.contains('q') || lowerValue.contains('transistor')) {
|
|
return 'transistor';
|
|
} else if (lowerValue.contains('u') || lowerValue.contains('ic')) {
|
|
return 'ic';
|
|
}
|
|
|
|
return 'custom';
|
|
}
|
|
|
|
String _convertPinType(String kicadType) {
|
|
switch (kicadType) {
|
|
case 'input': return 'input';
|
|
case 'output': return 'output';
|
|
case 'bidirectional': return 'bidirectional';
|
|
case 'power_in': return 'power';
|
|
case 'power_out': return 'power';
|
|
default: return 'passive';
|
|
}
|
|
}
|
|
|
|
String _generateId(String prefix) {
|
|
return '$prefix-${DateTime.now().millisecondsSinceEpoch}-${(DateTime.now().microsecondsSinceEpoch % 10000).toString().padLeft(4, '0')}';
|
|
}
|
|
|
|
// ============================================================================
|
|
// 公共 API
|
|
// ============================================================================
|
|
|
|
/// 从文件内容导入 KiCad 原理图
|
|
Map<String, dynamic> importKicadSchematic(String content, {String? format}) {
|
|
final importer = KicadImporter();
|
|
|
|
KicadSchematic kicadSch;
|
|
|
|
if (format == 'legacy') {
|
|
kicadSch = importer.importLegacySch(content);
|
|
} else {
|
|
kicadSch = importer.import(content);
|
|
}
|
|
|
|
return kicadToDesign(kicadSch);
|
|
}
|
|
|
|
/// 从文件路径导入 KiCad 原理图 (需要文件系统访问)
|
|
Future<Map<String, dynamic>> importKicadFile(String filePath) async {
|
|
// 实际实现需要文件读取
|
|
throw UnimplementedError('File system access not implemented');
|
|
}
|