# 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 = [] # List of {'uuid': str, 'class': str} self.stored_items = [] # List of filenames (str) pointing to item JSONs 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 _clear_selection_safe(self): try: sel = pcbnew.GetCurrentSelection() items = [i for i in sel] for item in items: if hasattr(item, 'ClearSelected'): item.ClearSelected() if hasattr(item, 'ClearBrightened'): item.ClearBrightened() except: pass def hide(self): self.manager.log(f"Layer {self.name}: Hiding...") try: board = pcbnew.GetBoard() self._clear_selection_safe() pcbnew.Refresh() new_stored_items = list(self.stored_items) items_to_remove = [] failed_entries = [] for entry in self.on_board_items: try: uuid = entry['uuid'] item = find_item_by_uuid(board, uuid) if not item: self.manager.log(f"Hide: Item {uuid} not found (skipping)") continue sexpr, err = Serializer.serialize(item) if not sexpr: self.manager.log(f"Hide: Serialize failed {uuid}: {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: new_stored_items.append(filename) items_to_remove.append(item) else: failed_entries.append(entry) except Exception as e: self.manager.log(f"Hide: Error entry {entry}: {e}") failed_entries.append(entry) for item in items_to_remove: try: if item.IsLocked(): item.SetLocked(False) board.Remove(item) except: pass 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("Hide: Complete") except Exception as e: self.manager.log(f"Hide: Critical Error: {e}") self.manager.log(traceback.format_exc()) def show(self): try: self.manager.log(f"Layer {self.name}: Showing (Force Restore)...") 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: try: item_data = self.manager.storage.load_item(filename) if not item_data: continue uuid = item_data.get('uuid') # Force Restore: Remove existing ghost if present existing = find_item_by_uuid(board, uuid) if existing: try: if existing.IsLocked(): existing.SetLocked(False) board.Remove(existing) except: pass item, err = Serializer.deserialize(item_data, board) if item: try: board.Add(item) 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) except Exception as e: self.manager.log(f"Show: Add failed {uuid}: {e}") except Exception as e: self.manager.log(f"Show: Error file {filename}: {e}") self.on_board_items = new_on_board_items for f in restored_files: if f in self.stored_items: self.stored_items.remove(f) self.visible = True self.manager.is_restoring = False self.manager.save() self.manager.log("Show: Complete") except Exception as e: self.manager.log(f"Show: Critical Error: {e}") self.manager.is_restoring = False def delete_content(self, delete_items_from_board): board = pcbnew.GetBoard() if self.visible: if delete_items_from_board: 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() 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): 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." def _get_selection_candidates(self, board): candidates = [] try: sel = pcbnew.GetCurrentSelection() for item in sel: candidates.append(item) except: pass if not candidates: # Fallback scan 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): 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." def remove_selection(self): 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." def inspect(self): return f"Layer: {self.name}\nVisible: {self.visible}\nOn Board: {len(self.on_board_items)}\nStored: {len(self.stored_items)}"