added windows interaction test

adding more variant stuff
This commit is contained in:
brentperteet
2026-02-22 13:31:14 -06:00
parent 3f0aff923d
commit ab8d5c0c14
6 changed files with 461 additions and 43 deletions

View File

@@ -6,7 +6,16 @@
"Bash(for uuid in \"12ef4843-0c6b-44b3-b52b-21b354565dc0\" \"17a476c2-1017-41e7-9d81-f4153fe179f7\" \"25a5bbfc-04ad-4755-9a82-80d42d2cd8ce\")", "Bash(for uuid in \"12ef4843-0c6b-44b3-b52b-21b354565dc0\" \"17a476c2-1017-41e7-9d81-f4153fe179f7\" \"25a5bbfc-04ad-4755-9a82-80d42d2cd8ce\")",
"Bash(do echo \"=== UUID: $uuid ===\")", "Bash(do echo \"=== UUID: $uuid ===\")",
"Bash(grep -A 15 \"$uuid\" 25w.kicad_sch frequency.kicad_sch)", "Bash(grep -A 15 \"$uuid\" 25w.kicad_sch frequency.kicad_sch)",
"Bash(done)" "Bash(done)",
"Bash(cat 25w.variants.json)",
"Bash(python sync_variant.py d:/tx/25w-kicad/25w/25w.kicad_sch default)",
"Bash(python -c \"\nimport json\nwith open(''25w.variants.json'', ''r'') as f:\n data = json.load(f)\n\n# Create V2 variant with different value for R1\ndata[''variants''][''V2''] = {\n ''name'': ''V2'',\n ''description'': ''Test variant with different R1 value'',\n ''dnp_parts'': [''4245045e-d174-4c56-920d-fabc03d1e234''],\n ''part_overrides'': {\n ''a2101067-196b-479f-99a2-dd5b5d884d82'': { # R1\n ''Value'': ''10M'' # Different from base 7.32M\n }\n }\n}\n\nwith open(''25w.variants.json'', ''w'') as f:\n json.dump(data, f, indent=2)\n\nprint(''Created V2 variant with R1 Value override'')\n\")",
"Bash(python apply_variant.py d:/tx/25w-kicad/25w/25w.kicad_sch V2)",
"Bash(python apply_variant.py d:/tx/25w-kicad/25w/25w.kicad_sch default)",
"Bash(python -c \"import pygetwindow; import pyautogui; print(''Libraries installed'')\")",
"Bash(pip install pygetwindow pyautogui)",
"Bash(python -c \"import pygetwindow; import pyautogui; print(''Libraries ready'')\")",
"Bash(python -c \"import win32gui; import win32con; print(''pywin32 installed'')\")"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

139
app.py
View File

@@ -721,6 +721,145 @@ def handle_sync_from_schematic():
except Exception as e: except Exception as e:
emit('variant_error', {'error': str(e)}) emit('variant_error', {'error': str(e)})
@socketio.on('test_window_interaction')
def handle_test_window_interaction():
try:
import pygetwindow as gw
import pyautogui
import time
import win32gui
import win32con
emit('window_test_status', {'status': 'Looking for KiCad schematic window...'})
# List all windows for debugging
all_windows = gw.getAllTitles()
emit('window_test_status', {'status': f'DEBUG: Found {len(all_windows)} windows total'})
# Find KiCad schematic editor window
windows = gw.getWindowsWithTitle('Schematic Editor')
emit('window_test_status', {'status': f'DEBUG: Found {len(windows)} windows with "Schematic Editor"'})
window_found = False
if not windows:
# Try alternative window title
schematic_windows = [w for w in all_windows if 'kicad' in w.lower() and 'schematic' in w.lower()]
emit('window_test_status', {'status': f'DEBUG: Found {len(schematic_windows)} windows with "kicad" and "schematic"'})
if schematic_windows:
emit('window_test_status', {'status': f'DEBUG: Using window: {schematic_windows[0]}'})
windows = gw.getWindowsWithTitle(schematic_windows[0])
window_found = len(windows) > 0
else:
window_found = True
# If window is found, close it
if window_found:
window = windows[0]
emit('window_test_status', {'status': f'Found window: "{window.title}"'})
# Get window position and size
hwnd = window._hWnd
rect = win32gui.GetWindowRect(hwnd)
x, y, x2, y2 = rect
width = x2 - x
height = y2 - y
emit('window_test_status', {'status': f'DEBUG: Window position=({x},{y}), size=({width}x{height})'})
# Click on the window's title bar to activate it (more reliable than SetForegroundWindow)
click_x = x + width // 2
click_y = y + 10 # Title bar is usually at the top
emit('window_test_status', {'status': f'Clicking window at ({click_x}, {click_y}) to activate...'})
pyautogui.click(click_x, click_y)
time.sleep(0.5)
emit('window_test_status', {'status': 'Sending Ctrl+S (save)...'})
pyautogui.hotkey('ctrl', 's')
time.sleep(1.0)
emit('window_test_status', {'status': 'Attempting to close window...'})
# Method 1: Try WM_CLOSE message
emit('window_test_status', {'status': 'DEBUG: Sending WM_CLOSE message...'})
win32gui.PostMessage(hwnd, win32con.WM_CLOSE, 0, 0)
time.sleep(1.0)
# Check if window still exists
if win32gui.IsWindow(hwnd):
emit('window_test_status', {'status': 'DEBUG: Window still exists after WM_CLOSE, trying to click and send Alt+F4...'})
# Click on window again to make sure it has focus
try:
rect = win32gui.GetWindowRect(hwnd)
x, y, x2, y2 = rect
click_x = x + (x2 - x) // 2
click_y = y + 10
pyautogui.click(click_x, click_y)
time.sleep(0.3)
except:
emit('window_test_status', {'status': 'DEBUG: Could not click window (may already be closed)'})
pyautogui.hotkey('alt', 'F4')
time.sleep(1.0)
# Final check
if win32gui.IsWindow(hwnd):
emit('window_test_status', {'status': 'DEBUG: Window still exists after Alt+F4 - may need manual intervention'})
else:
emit('window_test_status', {'status': 'DEBUG: Window closed successfully via Alt+F4'})
else:
emit('window_test_status', {'status': 'DEBUG: Window closed successfully via WM_CLOSE'})
else:
emit('window_test_status', {'status': 'No KiCad schematic window found - will open it'})
# Wait a couple seconds before reopening
emit('window_test_status', {'status': 'Waiting 2 seconds before reopening...'})
time.sleep(2.0)
# Reopen the schematic editor
schematic_file = app_args.get('Schematic File', '')
if not schematic_file:
emit('window_test_error', {'error': 'No schematic file specified in app arguments'})
return
emit('window_test_status', {'status': f'Relaunching schematic editor with: {schematic_file}'})
# Launch KiCad schematic editor
# The schematic editor executable is typically in the same directory as kicad.exe
import os
kicad_bin_dir = r"C:\Program Files\KiCad\9.0\bin" # Default KiCad 9 installation path
if not os.path.exists(kicad_bin_dir):
kicad_bin_dir = r"C:\Program Files\KiCad\8.0\bin" # Try KiCad 8
eeschema_exe = os.path.join(kicad_bin_dir, "eeschema.exe")
if not os.path.exists(eeschema_exe):
emit('window_test_error', {'error': f'KiCad executable not found at: {eeschema_exe}'})
return
emit('window_test_status', {'status': f'DEBUG: Launching {eeschema_exe} {schematic_file}'})
# Launch KiCad with the schematic file
result = subprocess.Popen([eeschema_exe, schematic_file], shell=False)
emit('window_test_status', {'status': f'DEBUG: Process started with PID {result.pid}'})
time.sleep(2.0)
# Verify the window opened
all_windows = gw.getAllTitles()
schematic_windows = [w for w in all_windows if 'kicad' in w.lower() and 'schematic' in w.lower()]
if schematic_windows:
emit('window_test_status', {'status': f'Successfully reopened schematic: {schematic_windows[0]}'})
else:
emit('window_test_status', {'status': 'Schematic editor launched but window not detected yet'})
emit('window_test_complete', {'message': 'Window interaction test completed!'})
except ImportError as e:
emit('window_test_error', {'error': f'Missing required library: {str(e)}. Please install: pip install pygetwindow pyautogui pywin32'})
except Exception as e:
import traceback
emit('window_test_error', {'error': f'{str(e)}\n\nTraceback:\n{traceback.format_exc()}'})
def shutdown_server(): def shutdown_server():
print("Server stopped") print("Server stopped")
os._exit(0) os._exit(0)

View File

@@ -76,8 +76,22 @@ def apply_variant_to_schematic(schematic_file: str, variant_name: str, kicad_cli
dnp_parts = manager.get_dnp_parts(variant_name) dnp_parts = manager.get_dnp_parts(variant_name)
# Build property overrides dict: uuid -> {base + variant_overrides}
property_overrides = {}
base_values = manager.variants.get("base_values", {})
variant_overrides = manager.variants["variants"][variant_name].get("part_overrides", {})
# Merge base + variant overrides for each part
all_uuids = set(list(base_values.keys()) + list(variant_overrides.keys()))
for uuid in all_uuids:
props = base_values.get(uuid, {}).copy()
props.update(variant_overrides.get(uuid, {}))
if props:
property_overrides[uuid] = props
print(f"Applying variant '{variant_name}' to {Path(schematic_file).name}") print(f"Applying variant '{variant_name}' to {Path(schematic_file).name}")
print(f"DNP parts ({len(dnp_parts)}): {dnp_parts}") print(f"DNP parts ({len(dnp_parts)}): {dnp_parts}")
print(f"Property overrides for {len(property_overrides)} parts")
# Get all schematic files (root + hierarchical sheets) # Get all schematic files (root + hierarchical sheets)
all_schematics = get_all_schematic_files(schematic_file) all_schematics = get_all_schematic_files(schematic_file)
@@ -88,7 +102,7 @@ def apply_variant_to_schematic(schematic_file: str, variant_name: str, kicad_cli
# Process each schematic file # Process each schematic file
for idx, sch_file in enumerate(all_schematics): for idx, sch_file in enumerate(all_schematics):
is_root = (idx == 0) # First file is the root schematic is_root = (idx == 0) # First file is the root schematic
if not process_single_schematic(sch_file, dnp_parts, variant_name, is_root): if not process_single_schematic(sch_file, dnp_parts, property_overrides, variant_name, is_root):
overall_success = False overall_success = False
if overall_success: if overall_success:
@@ -98,19 +112,22 @@ def apply_variant_to_schematic(schematic_file: str, variant_name: str, kicad_cli
return overall_success return overall_success
def process_single_schematic(schematic_file: str, dnp_uuids: list, variant_name: str = None, is_root: bool = False) -> bool: def process_single_schematic(schematic_file: str, dnp_uuids: list, property_overrides: dict = None, variant_name: str = None, is_root: bool = False) -> bool:
""" """
Process a single schematic file to apply DNP flags. Process a single schematic file to apply DNP flags and property overrides.
Args: Args:
schematic_file: Path to .kicad_sch file schematic_file: Path to .kicad_sch file
dnp_uuids: List of UUIDs that should be DNP dnp_uuids: List of UUIDs that should be DNP
property_overrides: Dict of UUID -> {property_name: value} for property overrides
variant_name: Name of variant being applied (for title block) variant_name: Name of variant being applied (for title block)
is_root: True if this is the root schematic (not a sub-sheet) is_root: True if this is the root schematic (not a sub-sheet)
Returns: Returns:
True if successful, False otherwise True if successful, False otherwise
""" """
if property_overrides is None:
property_overrides = {}
sch_path = Path(schematic_file) sch_path = Path(schematic_file)
if not sch_path.exists(): if not sch_path.exists():
print(f"Error: Schematic file not found: {schematic_file}") print(f"Error: Schematic file not found: {schematic_file}")
@@ -228,6 +245,70 @@ def process_single_schematic(schematic_file: str, dnp_uuids: list, variant_name:
else: else:
print(f" Cleared DNP: {current_ref if current_ref else current_uuid}") print(f" Cleared DNP: {current_ref if current_ref else current_uuid}")
# Apply property overrides
# Parse through symbols and update properties for parts that have overrides
if property_overrides:
in_symbol = False
current_uuid = None
current_ref = None
symbol_start_indent = None
for i, line in enumerate(lines):
stripped = line.strip()
indent_level = len(line) - len(line.lstrip())
# Detect start of symbol
if stripped.startswith('(symbol'):
in_symbol = True
current_uuid = None
current_ref = None
symbol_start_indent = indent_level
# Detect end of symbol (closing paren at same indent level)
elif in_symbol and stripped == ')' and indent_level == symbol_start_indent:
in_symbol = False
current_uuid = None
current_ref = None
# Extract UUID when in symbol (at symbol level, not nested)
elif in_symbol and '(dnp' in stripped and not current_uuid:
# Look forward for UUID after DNP line
for j in range(i + 1, min(len(lines), i + 5)):
if '(uuid' in lines[j]:
if '\t(uuid' in lines[j] or ' (uuid' in lines[j]:
uuid_parts = lines[j].split('"')
if len(uuid_parts) >= 2:
current_uuid = uuid_parts[1]
break
# Extract reference (for logging)
elif in_symbol and '(property "Reference"' in stripped and not current_ref:
ref_parts = line.split('"')
if len(ref_parts) >= 4:
current_ref = ref_parts[3]
# Check if this symbol has property overrides
elif in_symbol and current_uuid and current_uuid in property_overrides:
overrides = property_overrides[current_uuid]
# Check each tracked property
for prop_name in ['Value', 'MPN', 'Manufacturer', 'IPN']:
if prop_name in overrides and f'(property "{prop_name}"' in stripped:
# Extract current value
parts = line.split('"')
if len(parts) >= 4:
current_value = parts[3]
new_value = overrides[prop_name]
# Update if different
if current_value != new_value:
# Reconstruct the line with new value
indent = line[:len(line) - len(line.lstrip())]
parts[3] = new_value
lines[i] = indent + '"'.join(parts)
modified = True
print(f" Set {prop_name}: {current_ref if current_ref else current_uuid} = {new_value} (was {current_value})")
if modified: if modified:
# Backup original file # Backup original file
backup_path = sch_path.with_suffix('.kicad_sch.bak') backup_path = sch_path.with_suffix('.kicad_sch.bak')

View File

@@ -116,6 +116,7 @@ def sync_variant_from_schematic(schematic_file: str, target_variant: str = None)
all_dnp_uuids = [] all_dnp_uuids = []
all_uuids = [] all_uuids = []
all_part_properties = {} # uuid -> {property_name: value}
# Process each schematic file # Process each schematic file
for sch_file in all_schematics: for sch_file in all_schematics:
@@ -130,17 +131,21 @@ def sync_variant_from_schematic(schematic_file: str, target_variant: str = None)
with open(sch_path, 'r', encoding='utf-8') as f: with open(sch_path, 'r', encoding='utf-8') as f:
content = f.read() content = f.read()
# Parse schematic to find DNP components # Parse schematic to find DNP components and read properties
# Track properties: Value, MPN, Manufacturer, IPN
lines = content.split('\n') lines = content.split('\n')
in_symbol = False in_symbol = False
current_uuid = None current_uuid = None
current_ref = None current_ref = None
current_lib_id = None current_lib_id = None
current_properties = {} # Collect properties for this symbol
has_dnp = False has_dnp = False
# Track line depth to know when we're at symbol level # Track line depth to know when we're at symbol level
symbol_start_indent = None
for i, line in enumerate(lines): for i, line in enumerate(lines):
stripped = line.strip() stripped = line.strip()
indent_level = len(line) - len(line.lstrip())
# Detect start of symbol # Detect start of symbol
if stripped.startswith('(symbol'): if stripped.startswith('(symbol'):
@@ -148,20 +153,30 @@ def sync_variant_from_schematic(schematic_file: str, target_variant: str = None)
current_uuid = None current_uuid = None
current_ref = None current_ref = None
current_lib_id = None current_lib_id = None
current_properties = {}
has_dnp = False has_dnp = False
symbol_uuid_found = False # Track if we found the main symbol UUID symbol_uuid_found = False # Track if we found the main symbol UUID
symbol_start_indent = indent_level
# Detect end of symbol # Detect end of symbol - closing paren at same indent as symbol start
elif in_symbol and stripped == ')': elif in_symbol and stripped == ')' and indent_level == symbol_start_indent:
# Check if this symbol block is closing (simple heuristic)
# Skip power symbols # Skip power symbols
is_power = current_lib_id and 'power:' in current_lib_id is_power = current_lib_id and 'power:' in current_lib_id
is_power = is_power or (current_ref and current_ref.startswith('#')) is_power = is_power or (current_ref and current_ref.startswith('#'))
if current_uuid and has_dnp and not is_power: if current_uuid and not is_power:
if current_uuid not in all_dnp_uuids: # Store properties for this part
all_dnp_uuids.append(current_uuid) if current_properties:
print(f" Found DNP: {current_ref if current_ref else current_uuid}") if current_uuid not in all_part_properties:
all_part_properties[current_uuid] = {}
all_part_properties[current_uuid].update(current_properties)
all_part_properties[current_uuid]['reference'] = current_ref # For display
# Track DNP
if has_dnp:
if current_uuid not in all_dnp_uuids:
all_dnp_uuids.append(current_uuid)
print(f" Found DNP: {current_ref if current_ref else current_uuid}")
in_symbol = False in_symbol = False
# Extract lib_id to check for power symbols # Extract lib_id to check for power symbols
@@ -170,22 +185,23 @@ def sync_variant_from_schematic(schematic_file: str, target_variant: str = None)
if len(lib_parts) >= 2: if len(lib_parts) >= 2:
current_lib_id = lib_parts[1] current_lib_id = lib_parts[1]
# Check for DNP flag - can be (dnp), (dnp yes), or (dnp no) # Check for DNP flag and extract UUID
# Do this before UUID extraction so we know if we need the UUID # DNP line comes before UUID, so we look forward for the UUID
elif in_symbol and '(dnp' in stripped and not has_dnp: elif in_symbol and '(dnp' in stripped and not symbol_uuid_found:
# Only set has_dnp if it's (dnp) or (dnp yes), not (dnp no) # Check if DNP is set
if '(dnp yes)' in stripped or (stripped == '(dnp)'): if '(dnp yes)' in stripped or (stripped == '(dnp)'):
has_dnp = True has_dnp = True
# Now look forward for the UUID (it comes right after DNP)
for j in range(i + 1, min(len(lines), i + 5)): # Look forward for the UUID (it comes right after DNP)
if '(uuid' in lines[j]: for j in range(i + 1, min(len(lines), i + 5)):
# Check it's at symbol level if '(uuid' in lines[j]:
if '\t(uuid' in lines[j] or ' (uuid' in lines[j]: # Check it's at symbol level
uuid_parts = lines[j].split('"') if '\t(uuid' in lines[j] or ' (uuid' in lines[j]:
if len(uuid_parts) >= 2: uuid_parts = lines[j].split('"')
current_uuid = uuid_parts[1] if len(uuid_parts) >= 2:
symbol_uuid_found = True current_uuid = uuid_parts[1]
break symbol_uuid_found = True
break
# Extract reference designator (for logging) # Extract reference designator (for logging)
elif in_symbol and '(property "Reference"' in line and not current_ref: elif in_symbol and '(property "Reference"' in line and not current_ref:
@@ -194,6 +210,15 @@ def sync_variant_from_schematic(schematic_file: str, target_variant: str = None)
if len(parts) >= 4: if len(parts) >= 4:
current_ref = parts[3] current_ref = parts[3]
# Extract tracked properties
elif in_symbol:
for prop_name in ['Value', 'MPN', 'Manufacturer', 'IPN']:
if f'(property "{prop_name}"' in line:
parts = line.split('"')
if len(parts) >= 4:
current_properties[prop_name] = parts[3]
break
# Get all component UUIDs (excluding power symbols) # Get all component UUIDs (excluding power symbols)
# Use same approach - look for UUID after DNP line # Use same approach - look for UUID after DNP line
in_symbol = False in_symbol = False
@@ -238,31 +263,57 @@ def sync_variant_from_schematic(schematic_file: str, target_variant: str = None)
import traceback import traceback
traceback.print_exc() traceback.print_exc()
# Update variant with DNP list # Update variant with DNP list and property changes
print(f"\nUpdating variant '{active_variant}'...") print(f"\nUpdating variant '{active_variant}'...")
print(f" Found {len(all_uuids)} total UUIDs") print(f" Found {len(all_uuids)} total UUIDs")
print(f" Found {len(all_dnp_uuids)} DNP UUIDs") print(f" Found {len(all_dnp_uuids)} DNP UUIDs")
print(f" Found {len(all_part_properties)} parts with tracked properties")
# Build the new DNP list directly instead of calling set_part_dnp multiple times if active_variant not in manager.variants["variants"]:
# This avoids multiple file saves
if active_variant in manager.variants["variants"]:
# Set the DNP list directly
manager.variants["variants"][active_variant]["dnp_parts"] = sorted(all_dnp_uuids)
# Save once at the end
manager._save_variants()
print(f" Updated DNP list with {len(all_dnp_uuids)} parts")
for uuid in all_dnp_uuids:
print(f" DNP UUID: {uuid}")
else:
print(f" Error: Variant '{active_variant}' not found in variants") print(f" Error: Variant '{active_variant}' not found in variants")
return False return False
# Update DNP list
manager.variants["variants"][active_variant]["dnp_parts"] = sorted(all_dnp_uuids)
# Update property overrides based on schematic values
property_changes = 0
for uuid, sch_properties in all_part_properties.items():
# Get expected properties (base + variant overrides)
expected_props = manager.get_part_properties(active_variant, uuid)
# Check each tracked property
for prop_name in ['Value', 'MPN', 'Manufacturer', 'IPN']:
sch_value = sch_properties.get(prop_name, '')
expected_value = expected_props.get(prop_name, '')
# If schematic value differs from expected, update the variant
if sch_value and sch_value != expected_value:
# Check if we have a base value for this property
base_value = manager.variants.get("base_values", {}).get(uuid, {}).get(prop_name, '')
if not base_value:
# No base value exists, so set it
manager.set_base_value(uuid, prop_name, sch_value)
print(f" Set base {prop_name} for {sch_properties.get('reference', uuid)}: {sch_value}")
elif sch_value != base_value:
# Base value exists but differs - store as override
if "part_overrides" not in manager.variants["variants"][active_variant]:
manager.variants["variants"][active_variant]["part_overrides"] = {}
if uuid not in manager.variants["variants"][active_variant]["part_overrides"]:
manager.variants["variants"][active_variant]["part_overrides"][uuid] = {}
manager.variants["variants"][active_variant]["part_overrides"][uuid][prop_name] = sch_value
property_changes += 1
print(f" Override {prop_name} for {sch_properties.get('reference', uuid)}: {sch_value} (base: {base_value})")
# Save once at the end
manager._save_variants()
print(f"\nVariant '{active_variant}' updated:") print(f"\nVariant '{active_variant}' updated:")
print(f" Total components: {len(all_uuids)}") print(f" Total components: {len(all_uuids)}")
print(f" DNP components: {len(all_dnp_uuids)}") print(f" DNP components: {len(all_dnp_uuids)}")
print(f" Fitted components: {len(all_uuids) - len(all_dnp_uuids)}") print(f" Fitted components: {len(all_uuids) - len(all_dnp_uuids)}")
print(f" Property overrides: {property_changes}")
return True return True

View File

@@ -306,6 +306,12 @@
<div id="dbSyncMessage" class="message"></div> <div id="dbSyncMessage" class="message"></div>
</div> </div>
<div class="actions">
<h2>Window Testing</h2>
<button id="testWindowBtn" class="btn">Test: Save & Close Schematic Window</button>
<div id="windowTestMessage" class="message"></div>
</div>
<div class="actions"> <div class="actions">
<h2>System Initialization</h2> <h2>System Initialization</h2>
<button id="initUserBtn" class="btn">Initialize User Environment</button> <button id="initUserBtn" class="btn">Initialize User Environment</button>
@@ -835,6 +841,36 @@
loadVariants(); loadVariants();
}); });
// Window Testing
const testWindowBtn = document.getElementById('testWindowBtn');
const windowTestMessageEl = document.getElementById('windowTestMessage');
function showWindowTestMessage(text, type) {
windowTestMessageEl.textContent = text;
windowTestMessageEl.className = 'message ' + type;
windowTestMessageEl.style.display = 'block';
}
testWindowBtn.addEventListener('click', () => {
testWindowBtn.disabled = true;
showWindowTestMessage('Testing window interaction...', 'info');
socket.emit('test_window_interaction');
});
socket.on('window_test_status', (data) => {
showWindowTestMessage(data.status, 'info');
});
socket.on('window_test_complete', (data) => {
showWindowTestMessage(data.message, 'success');
testWindowBtn.disabled = false;
});
socket.on('window_test_error', (data) => {
showWindowTestMessage('Error: ' + data.error, 'error');
testWindowBtn.disabled = false;
});
// Close modals when clicking outside // Close modals when clicking outside
window.onclick = function(event) { window.onclick = function(event) {
if (event.target.className === 'modal') { if (event.target.className === 'modal') {

View File

@@ -35,19 +35,39 @@ class VariantManager:
"""Load variants from JSON file""" """Load variants from JSON file"""
if self.variants_file.exists(): if self.variants_file.exists():
with open(self.variants_file, 'r', encoding='utf-8') as f: with open(self.variants_file, 'r', encoding='utf-8') as f:
return json.load(f) data = json.load(f)
# Migrate from version 2 to version 3
if data.get("meta", {}).get("version", 1) < 3:
print(f"Migrating variant file from version {data.get('meta', {}).get('version', 1)} to 3")
if "base_values" not in data:
data["base_values"] = {}
for variant in data.get("variants", {}).values():
if "part_overrides" not in variant:
variant["part_overrides"] = {}
data["meta"]["version"] = 3
# Save migrated version
with open(self.variants_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
return data
else: else:
# Default structure # Default structure
return { return {
"meta": { "meta": {
"version": 2, # Version 2 uses UUIDs instead of references "version": 3, # Version 3 adds base_values and part_overrides
"active_variant": "default" "active_variant": "default"
}, },
"base_values": {
# UUID -> {property_name: value}
# Stores the base/default values for parts that vary between variants
},
"variants": { "variants": {
"default": { "default": {
"name": "default", "name": "default",
"description": "Default variant - all parts fitted", "description": "Default variant - all parts fitted",
"dnp_parts": [] # List of UUIDs that are DNP (Do Not Place) "dnp_parts": [], # List of UUIDs that are DNP (Do Not Place)
"part_overrides": {} # UUID -> {property_name: override_value}
} }
} }
} }
@@ -194,6 +214,88 @@ class VariantManager:
return uuid in self.variants["variants"][variant_name]["dnp_parts"] return uuid in self.variants["variants"][variant_name]["dnp_parts"]
def get_part_properties(self, variant_name: str, uuid: str) -> Dict[str, str]:
"""
Get effective property values for a part in a variant.
Merges base_values with variant-specific overrides.
Args:
variant_name: Variant name
uuid: Component UUID
Returns:
Dict of property_name -> value (merged base + overrides)
"""
if variant_name not in self.variants["variants"]:
return {}
# Start with base values
properties = self.variants.get("base_values", {}).get(uuid, {}).copy()
# Apply variant overrides
overrides = self.variants["variants"][variant_name].get("part_overrides", {}).get(uuid, {})
properties.update(overrides)
return properties
def set_part_property(self, variant_name: str, uuid: str, property_name: str, value: str) -> bool:
"""
Set a property override for a part in a variant.
Args:
variant_name: Variant name
uuid: Component UUID
property_name: Property to set (e.g., "Value", "MPN")
value: New value
Returns:
True if successful, False if variant doesn't exist
"""
if variant_name not in self.variants["variants"]:
return False
# Ensure part_overrides exists for this variant
if "part_overrides" not in self.variants["variants"][variant_name]:
self.variants["variants"][variant_name]["part_overrides"] = {}
# Get or create override dict for this UUID
if uuid not in self.variants["variants"][variant_name]["part_overrides"]:
self.variants["variants"][variant_name]["part_overrides"][uuid] = {}
# Check if this matches base value - if so, remove override (keep it sparse)
base_value = self.variants.get("base_values", {}).get(uuid, {}).get(property_name)
if value == base_value:
# Remove from overrides since it matches base
if property_name in self.variants["variants"][variant_name]["part_overrides"][uuid]:
del self.variants["variants"][variant_name]["part_overrides"][uuid][property_name]
# Clean up empty override dicts
if not self.variants["variants"][variant_name]["part_overrides"][uuid]:
del self.variants["variants"][variant_name]["part_overrides"][uuid]
else:
# Store override
self.variants["variants"][variant_name]["part_overrides"][uuid][property_name] = value
self._save_variants()
return True
def set_base_value(self, uuid: str, property_name: str, value: str):
"""
Set a base property value for a part.
Args:
uuid: Component UUID
property_name: Property name
value: Value to set
"""
if "base_values" not in self.variants:
self.variants["base_values"] = {}
if uuid not in self.variants["base_values"]:
self.variants["base_values"][uuid] = {}
self.variants["base_values"][uuid][property_name] = value
self._save_variants()
if __name__ == "__main__": if __name__ == "__main__":
if len(sys.argv) < 2: if len(sys.argv) < 2: