# backend/layer.py import pcbnew import re import traceback from .resolvers import get_item_uuid, resolve_to_trackable_item, find_item_by_uuid from .serializer import Serializer class Layer: def __init__(self, name, manager): self.name = name self.manager = manager self.visible = True self.on_board_items = [] self.stored_items = [] def add_item(self, item): if not item: return False uuid = get_item_uuid(item) if not uuid: return False cls = item.GetClass() for entry in self.on_board_items: if entry['uuid'] == uuid: return False self.on_board_items.append({'uuid': uuid, 'class': cls}) 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): try: sel = pcbnew.GetCurrentSelection() items = [i for i in sel] for item in items: self._safe_deselect(item) except: pass def hide(self): pcbnew.Refresh() # Brush teeth (Start) self.manager.log(f"Layer {self.name}: Hiding... ({len(self.on_board_items)} items)") try: 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() new_stored_items = list(self.stored_items) items_to_remove = [] failed_entries = [] # 3. Serialize for entry in self.on_board_items: try: uuid = entry['uuid'] self.manager.log(f" Processing {uuid}...") item = find_item_by_uuid(board, uuid) if not item: self.manager.log(f" Item not found on board (skipping)") continue sexpr, err = Serializer.serialize(item) if not sexpr: self.manager.log(f" Serialize failed: {err}") failed_entries.append(entry) continue item_data = {'class': item.GetClass(), 'sexpr': sexpr, 'uuid': uuid} if hasattr(item, 'GetReference'): item_data['ref'] = item.GetReference() if hasattr(item, 'GetNetname'): item_data['net'] = item.GetNetname() filename = self.manager.storage.save_item(item_data, uuid) if filename: self.manager.log(f" Saved to {filename}") new_stored_items.append(filename) items_to_remove.append(item) else: self.manager.log(f" Storage save failed") failed_entries.append(entry) except Exception as e: self.manager.log(f" Error processing entry: {e}") failed_entries.append(entry) # 4. Remove self.manager.log(f" Removing {len(items_to_remove)} items from board...") for item in items_to_remove: try: if item.IsLocked(): item.SetLocked(False) board.Remove(item) except Exception as e: self.manager.log(f" Remove failed: {e}") # 5. Update State self.stored_items = new_stored_items self.on_board_items = failed_entries if not self.on_board_items: self.visible = False self.manager.save() self.manager.log(f"Hide: Complete. Stored: {len(self.stored_items)}, Remaining on board: {len(self.on_board_items)}") except Exception as e: self.manager.log(f"Hide: Critical Error: {e}") self.manager.log(traceback.format_exc()) finally: pcbnew.Refresh() # Brush teeth (End) def show(self): pcbnew.Refresh() # Brush teeth (Start) try: self.manager.log(f"Layer {self.name}: Showing... Stored items: {len(self.stored_items)}") board = pcbnew.GetBoard() self.manager.is_restoring = True new_on_board_items = list(self.on_board_items) restored_files = [] for filename in self.stored_items: self.manager.log(f" Processing file {filename}...") try: item_data = self.manager.storage.load_item(filename) if not item_data: self.manager.log(f" Failed to load JSON data") continue uuid = item_data.get('uuid') self.manager.log(f" UUID: {uuid}") # Note: Ghost detection removed to prevent segfaults on stale pointers. # We assume items are not on board. item, err = Serializer.deserialize(item_data, board) if item: try: board.Add(item) self.manager.log(f" Board.Add success") new_uuid = get_item_uuid(item) if new_uuid: found = any(e['uuid'] == new_uuid for e in new_on_board_items) if not found: new_on_board_items.append({'uuid': new_uuid, 'class': item.GetClass()}) restored_files.append(filename) else: self.manager.log(f" Warning: Added item has no UUID") except Exception as e: self.manager.log(f" Board.Add failed: {e}") else: self.manager.log(f" Deserialize failed: {err}") except Exception as e: self.manager.log(f" Error processing file: {e}") self.manager.log(traceback.format_exc()) self.on_board_items = new_on_board_items # 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.manager.is_restoring = False self.manager.save() self.manager.log(f"Show: Complete. Restored: {len(restored_files)}, Failed/Remaining: {len(self.stored_items)}") except Exception as e: self.manager.log(f"Show: Critical Error: {e}") self.manager.log(traceback.format_exc()) self.manager.is_restoring = False finally: pcbnew.Refresh() # Brush teeth (End) def delete_content(self, delete_items_from_board): pcbnew.Refresh() # Brush teeth (Start) try: board = pcbnew.GetBoard() if self.visible: if delete_items_from_board: for entry in self.on_board_items: try: item = find_item_by_uuid(board, entry['uuid']) if item: self._safe_deselect(item) except: pass self._clear_selection_safe() for entry in self.on_board_items: item = find_item_by_uuid(board, entry['uuid']) if item: try: if item.IsLocked(): item.SetLocked(False) board.Remove(item) except: pass self.on_board_items = [] 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): self.delete_content(delete_items_from_board=True) def to_dict(self): return { "name": self.name, "visible": self.visible, "on_board_items": self.on_board_items, "stored_items": self.stored_items } @staticmethod def from_dict(d, manager): l = Layer(d["name"], manager) l.visible = d["visible"] l.on_board_items = d.get("on_board_items", []) l.stored_items = d.get("stored_items", []) return l def add_from_text(self, text): pcbnew.Refresh() # Brush teeth (Start) try: uuids = set(re.findall(r'\(uuid\s+"([^"]+)"\)', text)) if not uuids: return 0, "No UUIDs found." board = pcbnew.GetBoard() count = 0 for uuid in uuids: try: item = board.GetItem(pcbnew.KIID(uuid)) if item: trackable = resolve_to_trackable_item(item) if trackable and self.add_item(trackable): count += 1 except: pass self.manager.save() return count, f"Added {count} items." finally: pcbnew.Refresh() # Brush teeth (End) def _get_selection_candidates(self, board): candidates = [] try: sel = pcbnew.GetCurrentSelection() for item in sel: candidates.append(item) except: pass if not candidates: for lst in [board.GetTracks(), board.GetFootprints(), board.GetDrawings(), board.Zones(), board.Groups()]: try: for item in lst: if item.IsSelected(): candidates.append(item) except: pass return candidates def add_selection(self): pcbnew.Refresh() # Brush teeth (Start) try: board = pcbnew.GetBoard() candidates = self._get_selection_candidates(board) items_to_add = {} def process_item(item): if item.GetClass() == "PCB_GROUP": try: group = pcbnew.Cast_to_PCB_GROUP(item) for sub in group.GetItems(): process_item(sub) except: pass else: trackable = resolve_to_trackable_item(item) if trackable: uuid = get_item_uuid(trackable) if uuid: items_to_add[uuid] = trackable for item in candidates: process_item(item) count = 0 for uuid, item in items_to_add.items(): if self.add_item(item): count += 1 self.manager.save() return count, f"Added {count} items." finally: pcbnew.Refresh() # Brush teeth (End) def remove_selection(self): pcbnew.Refresh() # Brush teeth (Start) try: board = pcbnew.GetBoard() candidates = self._get_selection_candidates(board) uuids_to_remove = set() def collect_uuids(item): if item.GetClass() == "PCB_GROUP": try: group = pcbnew.Cast_to_PCB_GROUP(item) for sub in group.GetItems(): collect_uuids(sub) except: pass else: trackable = resolve_to_trackable_item(item) if trackable: uuid = get_item_uuid(trackable) if uuid: uuids_to_remove.add(uuid) for item in candidates: collect_uuids(item) 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] removed = initial_count - len(self.on_board_items) self.manager.save() return removed, f"Removed {removed} items." finally: pcbnew.Refresh() # Brush teeth (End) def inspect(self): return f"Layer: {self.name}\nVisible: {self.visible}\nOn Board: {len(self.on_board_items)}\nStored: {len(self.stored_items)}"