first commit where the whole thing works in at least common scenarios

This commit is contained in:
pszsh 2026-02-08 01:14:21 -08:00
parent 895139e14e
commit f64439f789
3 changed files with 320 additions and 180 deletions

View File

@ -10,8 +10,8 @@ class Layer:
self.name = name self.name = name
self.manager = manager self.manager = manager
self.visible = True self.visible = True
self.on_board_items = [] # List of {'uuid': str, 'class': str} self.on_board_items = []
self.stored_items = [] # List of filenames (str) pointing to item JSONs self.stored_items = []
def add_item(self, item): def add_item(self, item):
if not item: return False if not item: return False
@ -24,37 +24,58 @@ class Layer:
self.on_board_items.append({'uuid': uuid, 'class': cls}) self.on_board_items.append({'uuid': uuid, 'class': cls})
return True return True
def _safe_deselect(self, item):
if not item: return
try:
if hasattr(item, 'ClearSelected'): item.ClearSelected()
if hasattr(item, 'ClearBrightened'): item.ClearBrightened()
group = item.GetParentGroup()
if group:
if hasattr(group, 'ClearSelected'): group.ClearSelected()
if hasattr(group, 'ClearBrightened'): group.ClearBrightened()
except: pass
def _clear_selection_safe(self): def _clear_selection_safe(self):
try: try:
sel = pcbnew.GetCurrentSelection() sel = pcbnew.GetCurrentSelection()
items = [i for i in sel] items = [i for i in sel]
for item in items: for item in items:
if hasattr(item, 'ClearSelected'): item.ClearSelected() self._safe_deselect(item)
if hasattr(item, 'ClearBrightened'): item.ClearBrightened()
except: pass except: pass
def hide(self): def hide(self):
self.manager.log(f"Layer {self.name}: Hiding...") pcbnew.Refresh() # Brush teeth (Start)
self.manager.log(f"Layer {self.name}: Hiding... ({len(self.on_board_items)} items)")
try: try:
board = pcbnew.GetBoard() board = pcbnew.GetBoard()
# 1. Deselect specific items
for entry in self.on_board_items:
try:
item = find_item_by_uuid(board, entry['uuid'])
if item: self._safe_deselect(item)
except: pass
# 2. Clear general selection
self._clear_selection_safe() self._clear_selection_safe()
pcbnew.Refresh()
new_stored_items = list(self.stored_items) new_stored_items = list(self.stored_items)
items_to_remove = [] items_to_remove = []
failed_entries = [] failed_entries = []
# 3. Serialize
for entry in self.on_board_items: for entry in self.on_board_items:
try: try:
uuid = entry['uuid'] uuid = entry['uuid']
self.manager.log(f" Processing {uuid}...")
item = find_item_by_uuid(board, uuid) item = find_item_by_uuid(board, uuid)
if not item: if not item:
self.manager.log(f"Hide: Item {uuid} not found (skipping)") self.manager.log(f" Item not found on board (skipping)")
continue continue
sexpr, err = Serializer.serialize(item) sexpr, err = Serializer.serialize(item)
if not sexpr: if not sexpr:
self.manager.log(f"Hide: Serialize failed {uuid}: {err}") self.manager.log(f" Serialize failed: {err}")
failed_entries.append(entry) failed_entries.append(entry)
continue continue
@ -64,32 +85,42 @@ class Layer:
filename = self.manager.storage.save_item(item_data, uuid) filename = self.manager.storage.save_item(item_data, uuid)
if filename: if filename:
self.manager.log(f" Saved to {filename}")
new_stored_items.append(filename) new_stored_items.append(filename)
items_to_remove.append(item) items_to_remove.append(item)
else: else:
self.manager.log(f" Storage save failed")
failed_entries.append(entry) failed_entries.append(entry)
except Exception as e: except Exception as e:
self.manager.log(f"Hide: Error entry {entry}: {e}") self.manager.log(f" Error processing entry: {e}")
failed_entries.append(entry) failed_entries.append(entry)
# 4. Remove
self.manager.log(f" Removing {len(items_to_remove)} items from board...")
for item in items_to_remove: for item in items_to_remove:
try: try:
if item.IsLocked(): item.SetLocked(False) if item.IsLocked(): item.SetLocked(False)
board.Remove(item) board.Remove(item)
except: pass except Exception as e:
self.manager.log(f" Remove failed: {e}")
# 5. Update State
self.stored_items = new_stored_items self.stored_items = new_stored_items
self.on_board_items = failed_entries self.on_board_items = failed_entries
if not self.on_board_items: self.visible = False if not self.on_board_items: self.visible = False
self.manager.save() self.manager.save()
self.manager.log("Hide: Complete") self.manager.log(f"Hide: Complete. Stored: {len(self.stored_items)}, Remaining on board: {len(self.on_board_items)}")
except Exception as e: except Exception as e:
self.manager.log(f"Hide: Critical Error: {e}") self.manager.log(f"Hide: Critical Error: {e}")
self.manager.log(traceback.format_exc()) self.manager.log(traceback.format_exc())
finally:
pcbnew.Refresh() # Brush teeth (End)
def show(self): def show(self):
pcbnew.Refresh() # Brush teeth (Start)
try: try:
self.manager.log(f"Layer {self.name}: Showing (Force Restore)...") self.manager.log(f"Layer {self.name}: Showing... Stored items: {len(self.stored_items)}")
board = pcbnew.GetBoard() board = pcbnew.GetBoard()
self.manager.is_restoring = True self.manager.is_restoring = True
@ -97,70 +128,93 @@ class Layer:
restored_files = [] restored_files = []
for filename in self.stored_items: for filename in self.stored_items:
self.manager.log(f" Processing file {filename}...")
try: try:
item_data = self.manager.storage.load_item(filename) item_data = self.manager.storage.load_item(filename)
if not item_data: continue if not item_data:
uuid = item_data.get('uuid') self.manager.log(f" Failed to load JSON data")
continue
# Force Restore: Remove existing ghost if present uuid = item_data.get('uuid')
existing = find_item_by_uuid(board, uuid) self.manager.log(f" UUID: {uuid}")
if existing:
try: # Note: Ghost detection removed to prevent segfaults on stale pointers.
if existing.IsLocked(): existing.SetLocked(False) # We assume items are not on board.
board.Remove(existing)
except: pass
item, err = Serializer.deserialize(item_data, board) item, err = Serializer.deserialize(item_data, board)
if item: if item:
try: try:
board.Add(item) board.Add(item)
self.manager.log(f" Board.Add success")
new_uuid = get_item_uuid(item) new_uuid = get_item_uuid(item)
if new_uuid: if new_uuid:
found = any(e['uuid'] == new_uuid for e in new_on_board_items) found = any(e['uuid'] == new_uuid for e in new_on_board_items)
if not found: if not found:
new_on_board_items.append({'uuid': new_uuid, 'class': item.GetClass()}) new_on_board_items.append({'uuid': new_uuid, 'class': item.GetClass()})
restored_files.append(filename) restored_files.append(filename)
else:
self.manager.log(f" Warning: Added item has no UUID")
except Exception as e: except Exception as e:
self.manager.log(f"Show: Add failed {uuid}: {e}") self.manager.log(f" Board.Add failed: {e}")
else:
self.manager.log(f" Deserialize failed: {err}")
except Exception as e: except Exception as e:
self.manager.log(f"Show: Error file {filename}: {e}") self.manager.log(f" Error processing file: {e}")
self.manager.log(traceback.format_exc())
self.on_board_items = new_on_board_items self.on_board_items = new_on_board_items
for f in restored_files:
if f in self.stored_items: self.stored_items.remove(f) # Rebuild stored_items to only contain those that failed to restore
failed_files = [f for f in self.stored_items if f not in restored_files]
self.stored_items = failed_files
self.visible = True self.visible = True
self.manager.is_restoring = False self.manager.is_restoring = False
self.manager.save() self.manager.save()
self.manager.log("Show: Complete") self.manager.log(f"Show: Complete. Restored: {len(restored_files)}, Failed/Remaining: {len(self.stored_items)}")
except Exception as e: except Exception as e:
self.manager.log(f"Show: Critical Error: {e}") self.manager.log(f"Show: Critical Error: {e}")
self.manager.log(traceback.format_exc())
self.manager.is_restoring = False self.manager.is_restoring = False
finally:
pcbnew.Refresh() # Brush teeth (End)
def delete_content(self, delete_items_from_board): def delete_content(self, delete_items_from_board):
board = pcbnew.GetBoard() pcbnew.Refresh() # Brush teeth (Start)
if self.visible: try:
if delete_items_from_board: board = pcbnew.GetBoard()
self._clear_selection_safe() if self.visible:
for entry in self.on_board_items: if delete_items_from_board:
item = find_item_by_uuid(board, entry['uuid']) for entry in self.on_board_items:
if item:
try: try:
if item.IsLocked(): item.SetLocked(False) item = find_item_by_uuid(board, entry['uuid'])
board.Remove(item) if item: self._safe_deselect(item)
except: pass except: pass
self.on_board_items = []
else: self._clear_selection_safe()
if not delete_items_from_board:
self.manager.is_restoring = True for entry in self.on_board_items:
for filename in self.stored_items: item = find_item_by_uuid(board, entry['uuid'])
item_data = self.manager.storage.load_item(filename) if item:
if item_data: try:
item, err = Serializer.deserialize(item_data, board) if item.IsLocked(): item.SetLocked(False)
if item: board.Add(item) board.Remove(item)
self.manager.is_restoring = False except: pass
self.stored_items = [] self.on_board_items = []
self.manager.save() else:
if not delete_items_from_board:
self.manager.is_restoring = True
for filename in self.stored_items:
item_data = self.manager.storage.load_item(filename)
if item_data:
item, err = Serializer.deserialize(item_data, board)
if item: board.Add(item)
self.manager.is_restoring = False
self.stored_items = []
self.manager.save()
finally:
pcbnew.Refresh() # Brush teeth (End)
def clear_items(self): def clear_items(self):
self.delete_content(delete_items_from_board=True) self.delete_content(delete_items_from_board=True)
@ -180,19 +234,23 @@ class Layer:
return l return l
def add_from_text(self, text): def add_from_text(self, text):
uuids = set(re.findall(r'\(uuid\s+"([^"]+)"\)', text)) pcbnew.Refresh() # Brush teeth (Start)
if not uuids: return 0, "No UUIDs found." try:
board = pcbnew.GetBoard() uuids = set(re.findall(r'\(uuid\s+"([^"]+)"\)', text))
count = 0 if not uuids: return 0, "No UUIDs found."
for uuid in uuids: board = pcbnew.GetBoard()
try: count = 0
item = board.GetItem(pcbnew.KIID(uuid)) for uuid in uuids:
if item: try:
trackable = resolve_to_trackable_item(item) item = board.GetItem(pcbnew.KIID(uuid))
if trackable and self.add_item(trackable): count += 1 if item:
except: pass trackable = resolve_to_trackable_item(item)
self.manager.save() if trackable and self.add_item(trackable): count += 1
return count, f"Added {count} items." except: pass
self.manager.save()
return count, f"Added {count} items."
finally:
pcbnew.Refresh() # Brush teeth (End)
def _get_selection_candidates(self, board): def _get_selection_candidates(self, board):
candidates = [] candidates = []
@ -202,7 +260,6 @@ class Layer:
except: pass except: pass
if not candidates: if not candidates:
# Fallback scan
for lst in [board.GetTracks(), board.GetFootprints(), board.GetDrawings(), board.Zones(), board.Groups()]: for lst in [board.GetTracks(), board.GetFootprints(), board.GetDrawings(), board.Zones(), board.Groups()]:
try: try:
for item in lst: for item in lst:
@ -211,54 +268,62 @@ class Layer:
return candidates return candidates
def add_selection(self): def add_selection(self):
board = pcbnew.GetBoard() pcbnew.Refresh() # Brush teeth (Start)
candidates = self._get_selection_candidates(board) try:
items_to_add = {} board = pcbnew.GetBoard()
candidates = self._get_selection_candidates(board)
items_to_add = {}
def process_item(item): def process_item(item):
if item.GetClass() == "PCB_GROUP": if item.GetClass() == "PCB_GROUP":
try: try:
group = pcbnew.Cast_to_PCB_GROUP(item) group = pcbnew.Cast_to_PCB_GROUP(item)
for sub in group.GetItems(): process_item(sub) for sub in group.GetItems(): process_item(sub)
except: pass except: pass
else: else:
trackable = resolve_to_trackable_item(item) trackable = resolve_to_trackable_item(item)
if trackable: if trackable:
uuid = get_item_uuid(trackable) uuid = get_item_uuid(trackable)
if uuid: items_to_add[uuid] = trackable if uuid: items_to_add[uuid] = trackable
for item in candidates: process_item(item) for item in candidates: process_item(item)
count = 0 count = 0
for uuid, item in items_to_add.items(): for uuid, item in items_to_add.items():
if self.add_item(item): count += 1 if self.add_item(item): count += 1
self.manager.save() self.manager.save()
return count, f"Added {count} items." return count, f"Added {count} items."
finally:
pcbnew.Refresh() # Brush teeth (End)
def remove_selection(self): def remove_selection(self):
board = pcbnew.GetBoard() pcbnew.Refresh() # Brush teeth (Start)
candidates = self._get_selection_candidates(board) try:
uuids_to_remove = set() board = pcbnew.GetBoard()
candidates = self._get_selection_candidates(board)
uuids_to_remove = set()
def collect_uuids(item): def collect_uuids(item):
if item.GetClass() == "PCB_GROUP": if item.GetClass() == "PCB_GROUP":
try: try:
group = pcbnew.Cast_to_PCB_GROUP(item) group = pcbnew.Cast_to_PCB_GROUP(item)
for sub in group.GetItems(): collect_uuids(sub) for sub in group.GetItems(): collect_uuids(sub)
except: pass except: pass
else: else:
trackable = resolve_to_trackable_item(item) trackable = resolve_to_trackable_item(item)
if trackable: if trackable:
uuid = get_item_uuid(trackable) uuid = get_item_uuid(trackable)
if uuid: uuids_to_remove.add(uuid) if uuid: uuids_to_remove.add(uuid)
for item in candidates: collect_uuids(item) for item in candidates: collect_uuids(item)
initial_count = len(self.on_board_items) initial_count = len(self.on_board_items)
self.on_board_items = [e for e in self.on_board_items if e['uuid'] not in uuids_to_remove] self.on_board_items = [e for e in self.on_board_items if e['uuid'] not in uuids_to_remove]
removed = initial_count - len(self.on_board_items) removed = initial_count - len(self.on_board_items)
self.manager.save() self.manager.save()
return removed, f"Removed {removed} items." return removed, f"Removed {removed} items."
finally:
pcbnew.Refresh() # Brush teeth (End)
def inspect(self): def inspect(self):
return f"Layer: {self.name}\nVisible: {self.visible}\nOn Board: {len(self.on_board_items)}\nStored: {len(self.stored_items)}" return f"Layer: {self.name}\nVisible: {self.visible}\nOn Board: {len(self.on_board_items)}\nStored: {len(self.stored_items)}"

View File

@ -13,14 +13,37 @@ class LayerListener(pcbnew.BOARD_LISTENER):
self.mgr = mgr self.mgr = mgr
def OnBoardItemAdded(self, item): def OnBoardItemAdded(self, item):
if self.mgr.is_restoring: return # 1. Record State (The "Wake Up Tape")
if self.mgr.active_layer_index is not None: # Capture UUID before any refresh invalidates the item pointer or tool state.
try: target_uuid = get_item_uuid(item)
layer = self.mgr.layers[self.mgr.active_layer_index] if not target_uuid: return
if layer.visible:
trackable = resolve_to_trackable_item(item) # 2. Brush Teeth (Start) - Sync with reality
if trackable: layer.add_item(trackable) # This ensures we are facing the actual playing field.
except: pass pcbnew.Refresh()
try:
if self.mgr.is_restoring: return
# 3. Re-acquire State (Watch the tape)
# The 'item' pointer passed by the event might be unsafe now.
# We look up the item freshly from the board using the recorded UUID.
board = pcbnew.GetBoard()
fresh_item = find_item_by_uuid(board, target_uuid)
if not fresh_item: return
# 4. Perform Action
if self.mgr.active_layer_index is not None:
try:
layer = self.mgr.layers[self.mgr.active_layer_index]
if layer.visible:
trackable = resolve_to_trackable_item(fresh_item)
if trackable: layer.add_item(trackable)
except: pass
finally:
# 5. Brush Teeth (End) - Finalize
pcbnew.Refresh()
class LayerManager: class LayerManager:
def __init__(self): def __init__(self):
@ -108,74 +131,86 @@ class LayerManager:
return False return False
def analyze_clipboard_text(self, text): def analyze_clipboard_text(self, text):
self.log(f"Analyze: Input text length: {len(text)}") pcbnew.Refresh() # Start
uuids = set(re.findall(r'\(uuid\s+"([^"]+)"\)', text))
if not uuids: return self.get_log_text()
board = pcbnew.GetBoard()
matched = 0
def check(item):
try:
if get_item_uuid(item) in uuids: return 1
except: pass
return 0
try: try:
for lst in [board.GetTracks(), board.GetFootprints(), board.GetDrawings(), board.Zones()]: self.log(f"Analyze: Input text length: {len(text)}")
for item in lst: matched += check(item) uuids = set(re.findall(r'\(uuid\s+"([^"]+)"\)', text))
except: pass if not uuids: return self.get_log_text()
self.log(f"Analyze: Total Matches on Board: {matched}") board = pcbnew.GetBoard()
return self.get_log_text() matched = 0
def check(item):
try:
if get_item_uuid(item) in uuids: return 1
except: pass
return 0
try:
for lst in [board.GetTracks(), board.GetFootprints(), board.GetDrawings(), board.Zones()]:
for item in lst: matched += check(item)
except: pass
self.log(f"Analyze: Total Matches on Board: {matched}")
return self.get_log_text()
finally:
pcbnew.Refresh() # End
def recover_orphans(self): def recover_orphans(self):
if not self.storage.setup_paths(): return 0, "Storage not ready" pcbnew.Refresh() # Start
tracked_uuids = set()
stored_files = set()
board = pcbnew.GetBoard()
for l in self.layers:
for f in l.stored_items: stored_files.add(f)
for entry in l.on_board_items: tracked_uuids.add(entry['uuid'])
orphans = []
try: try:
for f in os.listdir(self.storage.session_dir): if not self.storage.setup_paths(): return 0, "Storage not ready"
if f.endswith(".json") and f != "$Master$.json" and f not in stored_files: tracked_uuids = set()
data = self.storage.load_item(f) stored_files = set()
if not data or 'uuid' not in data: continue board = pcbnew.GetBoard()
uuid = data['uuid'] for l in self.layers:
if uuid in tracked_uuids or find_item_by_uuid(board, uuid): continue for f in l.stored_items: stored_files.add(f)
orphans.append(f) for entry in l.on_board_items: tracked_uuids.add(entry['uuid'])
except Exception as e: return 0, f"Error scanning dir: {e}" orphans = []
if not orphans: return 0, "No orphans found" try:
rec_layer = self.create_layer("Recovered") for f in os.listdir(self.storage.session_dir):
rec_layer.stored_items = orphans if f.endswith(".json") and f != "$Master$.json" and f not in stored_files:
rec_layer.visible = False data = self.storage.load_item(f)
self.save() if not data or 'uuid' not in data: continue
return len(orphans), f"Recovered {len(orphans)} items." uuid = data['uuid']
if uuid in tracked_uuids or find_item_by_uuid(board, uuid): continue
orphans.append(f)
except Exception as e: return 0, f"Error scanning dir: {e}"
if not orphans: return 0, "No orphans found"
rec_layer = self.create_layer("Recovered")
rec_layer.stored_items = orphans
rec_layer.visible = False
self.save()
return len(orphans), f"Recovered {len(orphans)} items."
finally:
pcbnew.Refresh() # End
def hard_refresh(self): def hard_refresh(self):
self.log("Hard Refresh: Starting...") pcbnew.Refresh() # Start
board = pcbnew.GetBoard() try:
for layer in self.layers: self.log("Hard Refresh: Starting...")
if layer.visible: board = pcbnew.GetBoard()
valid_on_board = [] for layer in self.layers:
for entry in layer.on_board_items: if layer.visible:
uuid = entry['uuid'] valid_on_board = []
item = find_item_by_uuid(board, uuid) for entry in layer.on_board_items:
if item: uuid = entry['uuid']
sexpr, err = Serializer.serialize(item)
if sexpr: valid_on_board.append(entry)
else:
self.log(f"Refresh: Item {uuid} missing from visible layer {layer.name}")
layer.on_board_items = valid_on_board
else:
for filename in layer.stored_items:
data = self.storage.load_item(filename)
if data and 'uuid' in data:
uuid = data['uuid']
item = find_item_by_uuid(board, uuid) item = find_item_by_uuid(board, uuid)
if item: if item:
self.log(f"Refresh: Found ghost {uuid} from hidden layer {layer.name}. Removing.") sexpr, err = Serializer.serialize(item)
try: board.Remove(item) if sexpr: valid_on_board.append(entry)
except: pass else:
self.save() self.log(f"Refresh: Item {uuid} missing from visible layer {layer.name}")
self.log("Hard Refresh: Complete") layer.on_board_items = valid_on_board
else:
for filename in layer.stored_items:
data = self.storage.load_item(filename)
if data and 'uuid' in data:
uuid = data['uuid']
item = find_item_by_uuid(board, uuid)
if item:
self.log(f"Refresh: Found ghost {uuid} from hidden layer {layer.name}. Removing.")
try: board.Remove(item)
except: pass
self.save()
self.log("Hard Refresh: Complete")
finally:
pcbnew.Refresh() # End
manager = LayerManager() manager = LayerManager()

View File

@ -1,5 +1,6 @@
# backend/serializer.py # backend/serializer.py
import pcbnew import pcbnew
import traceback
class Serializer: class Serializer:
@staticmethod @staticmethod
@ -64,17 +65,56 @@ class Serializer:
except Exception as e: except Exception as e:
return None, f"Constructor failed for {cls_name}: {e}" return None, f"Constructor failed for {cls_name}: {e}"
# Deserialize with KiCad version compatibility # Ensure sexpr is a string
# KiCad 9+ requires (data, source_name), older versions take (data) if not isinstance(sexpr, str):
sexpr = str(sexpr)
# DEBUG LOGGING
print(f"[DEBUG] Deserializing {cls_name}")
print(f"[DEBUG] Sexpr (first 50 chars): {sexpr[:50]}")
lr = None
try: try:
# Primary method: KiCad 9+
lr = pcbnew.STRING_LINE_READER(sexpr, "restore") lr = pcbnew.STRING_LINE_READER(sexpr, "restore")
except: except TypeError:
# Fallback: Older KiCad versions (1 arg)
try: try:
lr = pcbnew.STRING_LINE_READER(sexpr) lr = pcbnew.STRING_LINE_READER(sexpr)
except Exception as e: except Exception as e:
return None, f"STRING_LINE_READER init failed: {e}" return None, f"STRING_LINE_READER init failed (Fallback): {e}"
except Exception as e:
return None, f"STRING_LINE_READER init failed: {e}"
if lr is None:
return None, "STRING_LINE_READER is None"
print(f"[DEBUG] LineReader created: {lr}")
try:
item.Deserialize(lr)
except Exception as e:
print(f"[DEBUG] Deserialize Exception: {e}")
# Check for the specific Protobuf overload mismatch
if "google::protobuf::Any" in str(e):
print("[DEBUG] Protobuf overload mismatch detected. Attempting PCB_IO_KICAD_SEXPR Parse fallback...")
try:
# Hail Mary: Try using the IO plugin directly to parse the string
# This bypasses the ambiguous Deserialize overload on the item itself
plugin = pcbnew.PCB_IO_KICAD_SEXPR()
if hasattr(plugin, 'Parse'):
# Parse usually returns a new item
parsed_item = plugin.Parse(sexpr)
if parsed_item:
print("[DEBUG] Fallback Parse successful!")
return parsed_item, None
except Exception as ex:
print(f"[DEBUG] Fallback Parse failed: {ex}")
# If fallback didn't work or wasn't applicable, re-raise
raise e
item.Deserialize(lr)
return item, None return item, None
except Exception as e: except Exception as e:
return None, f"Deserialization error for {cls_name}: {e}" return None, f"Deserialization error for {cls_name}: {e}"