fix: generate_netlist schematic_path, PinLocator cache, server.ts Python detection
- connection_schematic.py: generate_netlist() now accepts schematic_path param,
threaded through to get_net_connections() so PinLocator is actually invoked
(previously only 1 connection per component was returned due to fallback break)
- kicad_interface.py: pass schematic_path to generate_netlist()
- pin_locator.py: add _schematic_cache to avoid loading Schematic() once per pin
(was causing timeout: O(nets x components x pins) Schematic() calls)
- server.ts: remove fragile PYTHONPATH?.includes('KiCad') condition,
always prefer KiCAD bundled Python on Windows when executable exists
- CHANGELOG.md: document fixes under v2.2.0-alpha
This commit is contained in:
@@ -10,11 +10,13 @@ logger = logging.getLogger(__name__)
|
||||
try:
|
||||
from commands.wire_manager import WireManager
|
||||
from commands.pin_locator import PinLocator
|
||||
|
||||
WIRE_MANAGER_AVAILABLE = True
|
||||
except ImportError:
|
||||
logger.warning("WireManager/PinLocator not available")
|
||||
WIRE_MANAGER_AVAILABLE = False
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
"""Manage connections between components in schematics"""
|
||||
|
||||
@@ -29,7 +31,12 @@ class ConnectionManager:
|
||||
return cls._pin_locator
|
||||
|
||||
@staticmethod
|
||||
def add_wire(schematic_path: Path, start_point: list, end_point: list, properties: dict = None):
|
||||
def add_wire(
|
||||
schematic_path: Path,
|
||||
start_point: list,
|
||||
end_point: list,
|
||||
properties: dict = None,
|
||||
):
|
||||
"""
|
||||
Add a wire between two points using WireManager
|
||||
|
||||
@@ -47,11 +54,18 @@ class ConnectionManager:
|
||||
logger.error("WireManager not available")
|
||||
return False
|
||||
|
||||
stroke_width = properties.get('stroke_width', 0) if properties else 0
|
||||
stroke_type = properties.get('stroke_type', 'default') if properties else 'default'
|
||||
stroke_width = properties.get("stroke_width", 0) if properties else 0
|
||||
stroke_type = (
|
||||
properties.get("stroke_type", "default") if properties else "default"
|
||||
)
|
||||
|
||||
success = WireManager.add_wire(schematic_path, start_point, end_point,
|
||||
stroke_width=stroke_width, stroke_type=stroke_type)
|
||||
success = WireManager.add_wire(
|
||||
schematic_path,
|
||||
start_point,
|
||||
end_point,
|
||||
stroke_width=stroke_width,
|
||||
stroke_type=stroke_type,
|
||||
)
|
||||
return success
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding wire: {e}")
|
||||
@@ -70,7 +84,7 @@ class ConnectionManager:
|
||||
[x, y] coordinates or None if pin not found
|
||||
"""
|
||||
try:
|
||||
if not hasattr(symbol, 'pin'):
|
||||
if not hasattr(symbol, "pin"):
|
||||
logger.warning(f"Symbol {symbol.property.Reference.value} has no pins")
|
||||
return None
|
||||
|
||||
@@ -82,7 +96,9 @@ class ConnectionManager:
|
||||
break
|
||||
|
||||
if not target_pin:
|
||||
logger.warning(f"Pin '{pin_name}' not found on {symbol.property.Reference.value}")
|
||||
logger.warning(
|
||||
f"Pin '{pin_name}' not found on {symbol.property.Reference.value}"
|
||||
)
|
||||
return None
|
||||
|
||||
# Get pin location relative to symbol
|
||||
@@ -101,8 +117,14 @@ class ConnectionManager:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def add_connection(schematic_path: Path, source_ref: str, source_pin: str,
|
||||
target_ref: str, target_pin: str, routing: str = 'direct'):
|
||||
def add_connection(
|
||||
schematic_path: Path,
|
||||
source_ref: str,
|
||||
source_pin: str,
|
||||
target_ref: str,
|
||||
target_pin: str,
|
||||
routing: str = "direct",
|
||||
):
|
||||
"""
|
||||
Add a wire connection between two component pins
|
||||
|
||||
@@ -128,31 +150,41 @@ class ConnectionManager:
|
||||
return False
|
||||
|
||||
# Get pin locations
|
||||
source_loc = locator.get_pin_location(schematic_path, source_ref, source_pin)
|
||||
target_loc = locator.get_pin_location(schematic_path, target_ref, target_pin)
|
||||
source_loc = locator.get_pin_location(
|
||||
schematic_path, source_ref, source_pin
|
||||
)
|
||||
target_loc = locator.get_pin_location(
|
||||
schematic_path, target_ref, target_pin
|
||||
)
|
||||
|
||||
if not source_loc or not target_loc:
|
||||
logger.error("Could not determine pin locations")
|
||||
return False
|
||||
|
||||
# Create wire based on routing style
|
||||
if routing == 'direct':
|
||||
if routing == "direct":
|
||||
# Simple direct wire
|
||||
success = WireManager.add_wire(schematic_path, source_loc, target_loc)
|
||||
elif routing == 'orthogonal_h':
|
||||
elif routing == "orthogonal_h":
|
||||
# Orthogonal routing (horizontal first)
|
||||
path = WireManager.create_orthogonal_path(source_loc, target_loc, prefer_horizontal_first=True)
|
||||
path = WireManager.create_orthogonal_path(
|
||||
source_loc, target_loc, prefer_horizontal_first=True
|
||||
)
|
||||
success = WireManager.add_polyline_wire(schematic_path, path)
|
||||
elif routing == 'orthogonal_v':
|
||||
elif routing == "orthogonal_v":
|
||||
# Orthogonal routing (vertical first)
|
||||
path = WireManager.create_orthogonal_path(source_loc, target_loc, prefer_horizontal_first=False)
|
||||
path = WireManager.create_orthogonal_path(
|
||||
source_loc, target_loc, prefer_horizontal_first=False
|
||||
)
|
||||
success = WireManager.add_polyline_wire(schematic_path, path)
|
||||
else:
|
||||
logger.error(f"Unknown routing style: {routing}")
|
||||
return False
|
||||
|
||||
if success:
|
||||
logger.info(f"Connected {source_ref}/{source_pin} to {target_ref}/{target_pin} (routing: {routing})")
|
||||
logger.info(
|
||||
f"Connected {source_ref}/{source_pin} to {target_ref}/{target_pin} (routing: {routing})"
|
||||
)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -160,6 +192,7 @@ class ConnectionManager:
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding connection: {e}")
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
|
||||
@@ -177,13 +210,12 @@ class ConnectionManager:
|
||||
Label object or None on error
|
||||
"""
|
||||
try:
|
||||
if not hasattr(schematic, 'label'):
|
||||
if not hasattr(schematic, "label"):
|
||||
logger.error("Schematic does not have label collection")
|
||||
return None
|
||||
|
||||
label = schematic.label.append(
|
||||
text=net_name,
|
||||
at={'x': position[0], 'y': position[1]}
|
||||
text=net_name, at={"x": position[0], "y": position[1]}
|
||||
)
|
||||
logger.info(f"Added net label '{net_name}' at {position}")
|
||||
return label
|
||||
@@ -192,7 +224,9 @@ class ConnectionManager:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def connect_to_net(schematic_path: Path, component_ref: str, pin_name: str, net_name: str):
|
||||
def connect_to_net(
|
||||
schematic_path: Path, component_ref: str, pin_name: str, net_name: str
|
||||
):
|
||||
"""
|
||||
Connect a component pin to a named net using a wire stub and label
|
||||
|
||||
@@ -231,7 +265,9 @@ class ConnectionManager:
|
||||
return False
|
||||
|
||||
# Add label at the end of the stub using WireManager
|
||||
label_success = WireManager.add_label(schematic_path, net_name, stub_end, label_type='label')
|
||||
label_success = WireManager.add_label(
|
||||
schematic_path, net_name, stub_end, label_type="label"
|
||||
)
|
||||
if not label_success:
|
||||
logger.error(f"Failed to add net label '{net_name}'")
|
||||
return False
|
||||
@@ -242,11 +278,14 @@ class ConnectionManager:
|
||||
except Exception as e:
|
||||
logger.error(f"Error connecting to net: {e}")
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_net_connections(schematic: Schematic, net_name: str, schematic_path: Optional[Path] = None):
|
||||
def get_net_connections(
|
||||
schematic: Schematic, net_name: str, schematic_path: Optional[Path] = None
|
||||
):
|
||||
"""
|
||||
Get all connections for a named net using wire graph analysis
|
||||
|
||||
@@ -273,14 +312,14 @@ class ConnectionManager:
|
||||
return dx < tolerance and dy < tolerance
|
||||
|
||||
# 1. Find all labels with this net name
|
||||
if not hasattr(schematic, 'label'):
|
||||
if not hasattr(schematic, "label"):
|
||||
logger.warning("Schematic has no labels")
|
||||
return connections
|
||||
|
||||
net_label_positions = []
|
||||
for label in schematic.label:
|
||||
if hasattr(label, 'value') and label.value == net_name:
|
||||
if hasattr(label, 'at') and hasattr(label.at, 'value'):
|
||||
if hasattr(label, "value") and label.value == net_name:
|
||||
if hasattr(label, "at") and hasattr(label.at, "value"):
|
||||
pos = label.at.value
|
||||
net_label_positions.append([float(pos[0]), float(pos[1])])
|
||||
|
||||
@@ -288,21 +327,25 @@ class ConnectionManager:
|
||||
logger.info(f"No labels found for net '{net_name}'")
|
||||
return connections
|
||||
|
||||
logger.debug(f"Found {len(net_label_positions)} labels for net '{net_name}'")
|
||||
logger.debug(
|
||||
f"Found {len(net_label_positions)} labels for net '{net_name}'"
|
||||
)
|
||||
|
||||
# 2. Find all wires connected to these label positions
|
||||
if not hasattr(schematic, 'wire'):
|
||||
if not hasattr(schematic, "wire"):
|
||||
logger.warning("Schematic has no wires")
|
||||
return connections
|
||||
|
||||
connected_wire_points = set()
|
||||
for wire in schematic.wire:
|
||||
if hasattr(wire, 'pts') and hasattr(wire.pts, 'xy'):
|
||||
if hasattr(wire, "pts") and hasattr(wire.pts, "xy"):
|
||||
# Get all points in this wire (polyline)
|
||||
wire_points = []
|
||||
for point in wire.pts.xy:
|
||||
if hasattr(point, 'value'):
|
||||
wire_points.append([float(point.value[0]), float(point.value[1])])
|
||||
if hasattr(point, "value"):
|
||||
wire_points.append(
|
||||
[float(point.value[0]), float(point.value[1])]
|
||||
)
|
||||
|
||||
# Check if any wire point touches a label
|
||||
wire_connected = False
|
||||
@@ -323,10 +366,12 @@ class ConnectionManager:
|
||||
logger.debug(f"No wires connected to net '{net_name}' labels")
|
||||
return connections
|
||||
|
||||
logger.debug(f"Found {len(connected_wire_points)} wire connection points for net '{net_name}'")
|
||||
logger.debug(
|
||||
f"Found {len(connected_wire_points)} wire connection points for net '{net_name}'"
|
||||
)
|
||||
|
||||
# 3. Find component pins at wire endpoints
|
||||
if not hasattr(schematic, 'symbol'):
|
||||
if not hasattr(schematic, "symbol"):
|
||||
logger.warning("Schematic has no symbols")
|
||||
return connections
|
||||
|
||||
@@ -337,15 +382,15 @@ class ConnectionManager:
|
||||
|
||||
for symbol in schematic.symbol:
|
||||
# Skip template symbols
|
||||
if not hasattr(symbol.property, 'Reference'):
|
||||
if not hasattr(symbol.property, "Reference"):
|
||||
continue
|
||||
|
||||
ref = symbol.property.Reference.value
|
||||
if ref.startswith('_TEMPLATE'):
|
||||
if ref.startswith("_TEMPLATE"):
|
||||
continue
|
||||
|
||||
# Get lib_id for pin location lookup
|
||||
lib_id = symbol.lib_id.value if hasattr(symbol, 'lib_id') else None
|
||||
lib_id = symbol.lib_id.value if hasattr(symbol, "lib_id") else None
|
||||
if not lib_id:
|
||||
continue
|
||||
|
||||
@@ -360,17 +405,18 @@ class ConnectionManager:
|
||||
# Check each pin
|
||||
for pin_num, pin_data in pins.items():
|
||||
# Get pin location
|
||||
pin_loc = locator.get_pin_location(schematic_path, ref, pin_num)
|
||||
pin_loc = locator.get_pin_location(
|
||||
schematic_path, ref, pin_num
|
||||
)
|
||||
if not pin_loc:
|
||||
continue
|
||||
|
||||
# Check if pin coincides with any wire point
|
||||
for wire_pt in connected_wire_points:
|
||||
if points_coincide(pin_loc, list(wire_pt)):
|
||||
connections.append({
|
||||
"component": ref,
|
||||
"pin": pin_num
|
||||
})
|
||||
connections.append(
|
||||
{"component": ref, "pin": pin_num}
|
||||
)
|
||||
break # Pin found, no need to check more wire points
|
||||
|
||||
except Exception as e:
|
||||
@@ -380,7 +426,7 @@ class ConnectionManager:
|
||||
|
||||
# Fallback: proximity-based matching if no PinLocator
|
||||
if not locator or not schematic_path:
|
||||
symbol_pos = symbol.at.value if hasattr(symbol, 'at') else None
|
||||
symbol_pos = symbol.at.value if hasattr(symbol, "at") else None
|
||||
if not symbol_pos:
|
||||
continue
|
||||
|
||||
@@ -389,12 +435,11 @@ class ConnectionManager:
|
||||
|
||||
# Check if symbol is near any wire point (within 10mm)
|
||||
for wire_pt in connected_wire_points:
|
||||
dist = ((symbol_x - wire_pt[0])**2 + (symbol_y - wire_pt[1])**2)**0.5
|
||||
dist = (
|
||||
(symbol_x - wire_pt[0]) ** 2 + (symbol_y - wire_pt[1]) ** 2
|
||||
) ** 0.5
|
||||
if dist < 10.0: # 10mm proximity threshold
|
||||
connections.append({
|
||||
"component": ref,
|
||||
"pin": "unknown"
|
||||
})
|
||||
connections.append({"component": ref, "pin": "unknown"})
|
||||
break # Only add once per component
|
||||
|
||||
logger.info(f"Found {len(connections)} connections for net '{net_name}'")
|
||||
@@ -403,14 +448,20 @@ class ConnectionManager:
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting net connections: {e}")
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def generate_netlist(schematic: Schematic):
|
||||
def generate_netlist(schematic: Schematic, schematic_path: Optional[Path] = None):
|
||||
"""
|
||||
Generate a netlist from the schematic
|
||||
|
||||
Args:
|
||||
schematic: Schematic object
|
||||
schematic_path: Optional path to schematic file (enables accurate pin matching
|
||||
via PinLocator; without it, only one connection per component is found)
|
||||
|
||||
Returns:
|
||||
Dictionary with net information:
|
||||
{
|
||||
@@ -431,47 +482,58 @@ class ConnectionManager:
|
||||
}
|
||||
"""
|
||||
try:
|
||||
netlist = {
|
||||
"nets": [],
|
||||
"components": []
|
||||
}
|
||||
netlist = {"nets": [], "components": []}
|
||||
|
||||
# Gather all components
|
||||
if hasattr(schematic, 'symbol'):
|
||||
if hasattr(schematic, "symbol"):
|
||||
for symbol in schematic.symbol:
|
||||
component_info = {
|
||||
"reference": symbol.property.Reference.value,
|
||||
"value": symbol.property.Value.value if hasattr(symbol.property, 'Value') else "",
|
||||
"footprint": symbol.property.Footprint.value if hasattr(symbol.property, 'Footprint') else ""
|
||||
"value": (
|
||||
symbol.property.Value.value
|
||||
if hasattr(symbol.property, "Value")
|
||||
else ""
|
||||
),
|
||||
"footprint": (
|
||||
symbol.property.Footprint.value
|
||||
if hasattr(symbol.property, "Footprint")
|
||||
else ""
|
||||
),
|
||||
}
|
||||
netlist["components"].append(component_info)
|
||||
|
||||
# Gather all nets from labels
|
||||
if hasattr(schematic, 'label'):
|
||||
if hasattr(schematic, "label"):
|
||||
net_names = set()
|
||||
for label in schematic.label:
|
||||
if hasattr(label, 'value'):
|
||||
if hasattr(label, "value"):
|
||||
net_names.add(label.value)
|
||||
|
||||
# For each net, get connections
|
||||
for net_name in net_names:
|
||||
connections = ConnectionManager.get_net_connections(schematic, net_name)
|
||||
connections = ConnectionManager.get_net_connections(
|
||||
schematic, net_name, schematic_path
|
||||
)
|
||||
if connections:
|
||||
netlist["nets"].append({
|
||||
"name": net_name,
|
||||
"connections": connections
|
||||
})
|
||||
netlist["nets"].append(
|
||||
{"name": net_name, "connections": connections}
|
||||
)
|
||||
|
||||
logger.info(f"Generated netlist with {len(netlist['nets'])} nets and {len(netlist['components'])} components")
|
||||
logger.info(
|
||||
f"Generated netlist with {len(netlist['nets'])} nets and {len(netlist['components'])} components"
|
||||
)
|
||||
return netlist
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating netlist: {e}")
|
||||
return {"nets": [], "components": []}
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example Usage (for testing)
|
||||
from schematic import SchematicManager # Assuming schematic.py is in the same directory
|
||||
from schematic import (
|
||||
SchematicManager,
|
||||
) # Assuming schematic.py is in the same directory
|
||||
|
||||
# Create a new schematic
|
||||
test_sch = SchematicManager.create_schematic("ConnectionTestSchematic")
|
||||
|
||||
Reference in New Issue
Block a user