329 lines
13 KiB
Python
329 lines
13 KiB
Python
# 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)}" |