Fixed manual image stitching and Inspector view for mobiles, other minor fixes/improvements.
This commit is contained in:
parent
55e55edee9
commit
ca9fe52db5
10 changed files with 803 additions and 618 deletions
315
docs/studio.js
315
docs/studio.js
|
|
@ -106,16 +106,20 @@ async function init() {
|
|||
db = new PcbDatabase();
|
||||
await db.init();
|
||||
|
||||
if (!history.state) {
|
||||
history.replaceState({ context: 'list' }, "", "");
|
||||
}
|
||||
|
||||
cvManager = new CVManager();
|
||||
cvManager.init(); // Lazy load (don't await)
|
||||
|
||||
// Init net managers
|
||||
netManager = new NetManager(db);
|
||||
inspector = new Inspector(db, cvManager);
|
||||
// Init net managers
|
||||
netManager = new NetManager(db);
|
||||
inspector = new Inspector(db, cvManager);
|
||||
|
||||
// [CRITICAL] Expose to window so inline HTML onclicks (generated by Inspector) work
|
||||
window.netManager = netManager;
|
||||
window.inspector = inspector;
|
||||
// [CRITICAL] Expose to window so inline HTML onclicks (generated by Inspector) work
|
||||
window.netManager = netManager;
|
||||
window.inspector = inspector;
|
||||
|
||||
stitchEditor = new StitchEditor(db, cvManager);
|
||||
|
||||
|
|
@ -218,7 +222,29 @@ async function switchBom() {
|
|||
}
|
||||
|
||||
async function loadProjectData() {
|
||||
if (!currentBomId) { bomData = []; bomImages = []; renderList(); return; }
|
||||
// Helper to refresh other tabs
|
||||
const refreshViews = async () => {
|
||||
if (window.netManager) window.netManager.render();
|
||||
if (window.inspector) {
|
||||
// Clear previous selection as IDs are no longer valid
|
||||
window.inspector.visibleIds.clear();
|
||||
window.inspector.activeNet = null; // Reset active net
|
||||
await window.inspector.init(); // Rebuild sidebar & grid
|
||||
}
|
||||
};
|
||||
|
||||
if (!currentBomId) {
|
||||
bomData = [];
|
||||
bomImages = [];
|
||||
renderList();
|
||||
|
||||
// FIX: Clear other views if no board selected
|
||||
const imgSel = document.getElementById('image-select');
|
||||
if(imgSel) imgSel.innerHTML = '<option disabled selected>No Images</option>';
|
||||
clearMap();
|
||||
await refreshViews();
|
||||
return;
|
||||
}
|
||||
|
||||
const meta = bomList.find(p=>p.id===currentBomId);
|
||||
if (meta) {
|
||||
|
|
@ -229,6 +255,7 @@ async function loadProjectData() {
|
|||
bomData = await db.getComponents(currentBomId);
|
||||
bomImages = await db.getImages(currentBomId);
|
||||
|
||||
// Convert raw blobs to Image objects for cache if needed (lazy loaded usually)
|
||||
for (const img of bomImages) {
|
||||
if (/\.(jpg|jpeg|png|webp)$/i.test(img.name)) {
|
||||
img.name = img.name.replace(/\.[^/.]+$/, "");
|
||||
|
|
@ -239,24 +266,30 @@ async function loadProjectData() {
|
|||
const imgSel = document.getElementById('image-select');
|
||||
imgSel.innerHTML = '';
|
||||
if(bomImages.length > 0) {
|
||||
bomImages.forEach(img => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = img.id; opt.innerText = img.name;
|
||||
imgSel.appendChild(opt);
|
||||
});
|
||||
if(!currentImgId || !bomImages.find(i=>i.id===currentImgId)) currentImgId = bomImages[0].id;
|
||||
imgSel.value = currentImgId;
|
||||
if(document.getElementById('view-map').classList.contains('active')) showImage(currentImgId);
|
||||
bomImages.forEach(img => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = img.id; opt.innerText = img.name;
|
||||
imgSel.appendChild(opt);
|
||||
});
|
||||
if(!currentImgId || !bomImages.find(i=>i.id===currentImgId)) currentImgId = bomImages[0].id;
|
||||
imgSel.value = currentImgId;
|
||||
|
||||
// Only load the image if we are actually on the map tab
|
||||
if(document.getElementById('view-map').classList.contains('active')) showImage(currentImgId);
|
||||
} else {
|
||||
currentImgId = null;
|
||||
imgSel.innerHTML = '<option disabled selected>No Images</option>';
|
||||
clearMap();
|
||||
currentImgId = null;
|
||||
imgSel.innerHTML = '<option disabled selected>No Images</option>';
|
||||
clearMap();
|
||||
}
|
||||
|
||||
renderList();
|
||||
|
||||
// Refresh Nets and Inspector with new data
|
||||
await refreshViews();
|
||||
}
|
||||
|
||||
async function createNewDevice() {
|
||||
const n = prompt("New Device Name:");
|
||||
const n = await requestInput("New Device", "Device Name", "");
|
||||
if (n) {
|
||||
const id = uuid();
|
||||
await db.addDevice({ id, name: n });
|
||||
|
|
@ -271,9 +304,9 @@ async function createNewDevice() {
|
|||
|
||||
async function createNewBom() {
|
||||
if (!currentDeviceId) return alert("Select a device first.");
|
||||
const n=prompt("New Board Name:");
|
||||
const n = await requestInput("New Board", "Board Name", "");
|
||||
if(n) {
|
||||
const sec = prompt("Section (Optional):") || "";
|
||||
const sec = await requestInput("Section", "Group (Optional)", "") || "";
|
||||
const id=uuid();
|
||||
await db.addProject({id, deviceId: currentDeviceId, name:n, section: sec, lastModified:Date.now(), sortMode:'none'});
|
||||
currentBomId=id;
|
||||
|
|
@ -437,9 +470,9 @@ async function exportDeviceZIP() {
|
|||
const dev = deviceList.find(d => d.id === currentDeviceId);
|
||||
const boms = await db.getProjectsByDevice(currentDeviceId);
|
||||
const zip = new JSZip();
|
||||
|
||||
// Fetch ALL nets once (since we don't have an index, we filter in JS)
|
||||
const allNets = await db.getNets();
|
||||
|
||||
// Fetch ALL nets once (since we don't have an index, we filter in JS)
|
||||
const allNets = await db.getNets();
|
||||
|
||||
// 1. Generate README
|
||||
const readmeContent = `PCB ReTrace Data Export
|
||||
|
|
@ -466,9 +499,9 @@ HOW TO USE:
|
|||
for (const bom of boms) {
|
||||
const comps = await db.getComponents(bom.id);
|
||||
const imgs = await db.getImages(bom.id);
|
||||
|
||||
// Filter nets for this specific board
|
||||
const boardNets = allNets.filter(n => n.projectId === bom.id);
|
||||
|
||||
// Filter nets for this specific board
|
||||
const boardNets = allNets.filter(n => n.projectId === bom.id);
|
||||
|
||||
const overlapsMap = new Map();
|
||||
for(const img of imgs) {
|
||||
|
|
@ -484,7 +517,7 @@ HOW TO USE:
|
|||
components: cleanComps,
|
||||
images: imgMeta,
|
||||
overlaps: Array.from(overlapsMap.values()),
|
||||
nets: boardNets // Add Nets to manifest
|
||||
nets: boardNets // Add Nets to manifest
|
||||
});
|
||||
|
||||
// Image Loop
|
||||
|
|
@ -746,8 +779,8 @@ async function restoreDevice(manifest, zip) {
|
|||
for (const lb of localBoards) {
|
||||
if (!manifestBoardIds.has(lb.id)) {
|
||||
await db.deleteProject(lb.id);
|
||||
// Note: deleteProject doesn't currently delete Nets automatically in the DB class
|
||||
// We should ideally clean them up, but for now we focus on the restore logic.
|
||||
// Note: deleteProject doesn't currently delete Nets automatically in the DB class
|
||||
// We should ideally clean them up, but for now we focus on the restore logic.
|
||||
console.log(`Pruned Board: ${lb.name}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -801,13 +834,13 @@ async function restoreDevice(manifest, zip) {
|
|||
});
|
||||
}
|
||||
|
||||
// 4. Prune Nets [NEW]
|
||||
const allNets = await db.getNets();
|
||||
const localNets = allNets.filter(n => n.projectId === boardId);
|
||||
const importNetIds = new Set((boardData.nets || []).map(n => n.id));
|
||||
for(const ln of localNets) {
|
||||
if(!importNetIds.has(ln.id)) await db.deleteNet(ln.id);
|
||||
}
|
||||
// 4. Prune Nets [NEW]
|
||||
const allNets = await db.getNets();
|
||||
const localNets = allNets.filter(n => n.projectId === boardId);
|
||||
const importNetIds = new Set((boardData.nets || []).map(n => n.id));
|
||||
for(const ln of localNets) {
|
||||
if(!importNetIds.has(ln.id)) await db.deleteNet(ln.id);
|
||||
}
|
||||
|
||||
} else {
|
||||
mergedBoards++;
|
||||
|
|
@ -863,19 +896,19 @@ async function restoreDevice(manifest, zip) {
|
|||
}
|
||||
}
|
||||
|
||||
// Nets [NEW]
|
||||
if (boardData.nets) {
|
||||
for (const net of boardData.nets) {
|
||||
// Ensure correct association
|
||||
net.projectId = boardId;
|
||||
if (isNewer) {
|
||||
await db.addNet(net);
|
||||
} else {
|
||||
const existingNet = await db._tx('nets', 'readonly', s => s.get(net.id));
|
||||
if (!existingNet) await db.addNet(net);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Nets [NEW]
|
||||
if (boardData.nets) {
|
||||
for (const net of boardData.nets) {
|
||||
// Ensure correct association
|
||||
net.projectId = boardId;
|
||||
if (isNewer) {
|
||||
await db.addNet(net);
|
||||
} else {
|
||||
const existingNet = await db._tx('nets', 'readonly', s => s.get(net.id));
|
||||
if (!existingNet) await db.addNet(net);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deviceList = await db.getDevices();
|
||||
|
|
@ -928,13 +961,13 @@ async function processImportData(data, zipObj) {
|
|||
});
|
||||
}
|
||||
|
||||
// 4. Nets [NEW]
|
||||
const allNets = await db.getNets();
|
||||
const localNets = allNets.filter(n => n.projectId === boardId);
|
||||
const importNetIds = new Set((data.nets || []).map(n => n.id));
|
||||
for(const ln of localNets) {
|
||||
if(!importNetIds.has(ln.id)) await db.deleteNet(ln.id);
|
||||
}
|
||||
// 4. Nets [NEW]
|
||||
const allNets = await db.getNets();
|
||||
const localNets = allNets.filter(n => n.projectId === boardId);
|
||||
const importNetIds = new Set((data.nets || []).map(n => n.id));
|
||||
for(const ln of localNets) {
|
||||
if(!importNetIds.has(ln.id)) await db.deleteNet(ln.id);
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log("Import is older/same. Merging missing items only.");
|
||||
|
|
@ -994,18 +1027,18 @@ async function processImportData(data, zipObj) {
|
|||
}
|
||||
}
|
||||
|
||||
// Nets [NEW]
|
||||
if (data.nets) {
|
||||
for (const net of data.nets) {
|
||||
net.projectId = boardId;
|
||||
if (isNewer) {
|
||||
await db.addNet(net);
|
||||
} else {
|
||||
const existing = await db._tx('nets', 'readonly', s => s.get(net.id));
|
||||
if (!existing) await db.addNet(net);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Nets [NEW]
|
||||
if (data.nets) {
|
||||
for (const net of data.nets) {
|
||||
net.projectId = boardId;
|
||||
if (isNewer) {
|
||||
await db.addNet(net);
|
||||
} else {
|
||||
const existing = await db._tx('nets', 'readonly', s => s.get(net.id));
|
||||
if (!existing) await db.addNet(net);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await loadDeviceBoms();
|
||||
}
|
||||
|
|
@ -1023,9 +1056,9 @@ async function exportZIP() {
|
|||
const meta = bomList.find(x=>x.id===currentBomId);
|
||||
const images = await db.getImages(currentBomId);
|
||||
|
||||
// Fetch and filter Nets
|
||||
const allNets = await db.getNets();
|
||||
const boardNets = allNets.filter(n => n.projectId === currentBomId);
|
||||
// Fetch and filter Nets
|
||||
const allNets = await db.getNets();
|
||||
const boardNets = allNets.filter(n => n.projectId === currentBomId);
|
||||
|
||||
const overlapsMap = new Map();
|
||||
for(const img of images) {
|
||||
|
|
@ -1056,7 +1089,7 @@ HOW TO USE:
|
|||
components: cleanComps,
|
||||
images: imgMeta,
|
||||
overlaps: overlaps,
|
||||
nets: boardNets, // Add Nets
|
||||
nets: boardNets, // Add Nets
|
||||
version: DB_VER,
|
||||
source: "pcb.etaras.com"
|
||||
};
|
||||
|
|
@ -1141,10 +1174,10 @@ function switchView(v) {
|
|||
if(!document.getElementById('map-content').querySelector('.pcb-image')) showImage(currentImgId);
|
||||
else { setTimeout(() => { if(mapState.scale === 1 && mapState.x === 0 && mapState.y === 0) fitMap(); renderPins(); }, 50); }
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (v === 'nets') {
|
||||
if(window.netManager) window.netManager.render();
|
||||
}
|
||||
}
|
||||
else if (v === 'inspect') {
|
||||
if(window.inspector) window.inspector.init();
|
||||
}
|
||||
|
|
@ -1183,9 +1216,9 @@ function renderList() {
|
|||
view.forEach(p => {
|
||||
const pinIcon = (p.x !== undefined) ? `<span style="cursor:pointer" onclick="locateComponent('${p.imgId}', ${p.x}, ${p.y}); event.stopPropagation();">🎯</span>` : '';
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
|
||||
// [NEW] Assign ID for lookup
|
||||
tr.dataset.id = p.id;
|
||||
tr.dataset.id = p.id;
|
||||
|
||||
tr.onclick = (e) => {
|
||||
if(e.target.tagName==='BUTTON' || e.target.closest('button')) return;
|
||||
|
|
@ -1449,7 +1482,8 @@ window.saveProcessedImageToDB = saveProcessedImageToDB;
|
|||
|
||||
async function uploadImage(input) {
|
||||
const file = input.files[0]; if(!file) return;
|
||||
const name = prompt("Image Name:", file.name);
|
||||
const name = await requestInput("Upload Image", "Image Name", file.name);
|
||||
|
||||
if(!name) return;
|
||||
|
||||
const id = uuid(); // Capture ID to use later
|
||||
|
|
@ -1899,10 +1933,10 @@ async function fillFormFromData(c) {
|
|||
function copyPartToForm(idx) { const c = bomData[idx]; document.getElementById('inp-value').value = c.value; document.getElementById('inp-desc').value = c.desc; }
|
||||
|
||||
// --- CONNECTIONS UI ---
|
||||
// Delete connection function
|
||||
// Delete connection function
|
||||
async function deleteConnection(targetId) {
|
||||
if(!currentImgId || !targetId) return;
|
||||
|
||||
|
||||
const targetImg = bomImages.find(i => i.id === targetId);
|
||||
const name = targetImg ? targetImg.name : "this image";
|
||||
|
||||
|
|
@ -1919,24 +1953,24 @@ async function renderConnectionsList() {
|
|||
if(!currentImgId) return;
|
||||
const curImg = bomImages.find(i => i.id === currentImgId);
|
||||
if(!curImg) return;
|
||||
|
||||
|
||||
const title = document.getElementById('conn-src-name');
|
||||
if(title) title.innerText = curImg.name;
|
||||
|
||||
|
||||
const list = document.getElementById('conn-list');
|
||||
if(!list) return;
|
||||
|
||||
|
||||
list.innerHTML = '<div style="text-align:center;color:#888">Loading...</div>';
|
||||
|
||||
const overlaps = await db.getOverlapsForImage(currentImgId);
|
||||
const others = bomImages.filter(i => i.id !== currentImgId);
|
||||
|
||||
|
||||
list.innerHTML = '';
|
||||
if(others.length === 0) {
|
||||
list.innerHTML = '<div style="padding:1rem; text-align:center; background:#eee; border-radius:4px;">No other images to stitch with.</div>';
|
||||
return;
|
||||
if(others.length === 0) {
|
||||
list.innerHTML = '<div style="padding:1rem; text-align:center; background:#eee; border-radius:4px;">No other images to stitch with.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
others.forEach(img => {
|
||||
const ov = overlaps.find(o => o.fromImageId === img.id || o.toImageId === img.id);
|
||||
const row = document.createElement('div');
|
||||
|
|
@ -1947,7 +1981,7 @@ async function renderConnectionsList() {
|
|||
if(ov) {
|
||||
if(ov.isManual) statusBadge = `<span style="font-size:0.75rem; background:#dcfce7; color:#166534; padding:2px 6px; border-radius:4px;">Manual (${ov.matchCount} pts)</span>`;
|
||||
else statusBadge = `<span style="font-size:0.75rem; background:#e0f2fe; color:#0369a1; padding:2px 6px; border-radius:4px;">Auto-CV</span>`;
|
||||
|
||||
|
||||
// Show Delete + Edit buttons
|
||||
actions = `<div style="display:flex; gap:5px;">
|
||||
<button class="danger sm-btn" style="padding:0 8px;" onclick="deleteConnection('${img.id}')" title="Remove Connection">🗑️</button>
|
||||
|
|
@ -2241,10 +2275,10 @@ const NavManager = {
|
|||
} else {
|
||||
// Default to List
|
||||
switchView('list', true);
|
||||
|
||||
|
||||
// Restore Tool if needed
|
||||
if (ctx === 'tool') this.restoreTool();
|
||||
|
||||
|
||||
// Restore Modals
|
||||
if (ctx && ctx.endsWith('-modal')) {
|
||||
const el = document.getElementById(ctx);
|
||||
|
|
@ -2337,13 +2371,108 @@ const NavManager = {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic Input Dialog Helper
|
||||
* @param {string} title - Modal Title
|
||||
* @param {string} label - Input Field Label
|
||||
* @param {string} val - Default Value
|
||||
* @param {object} opts - { extraBtn: {label, value, class} }
|
||||
* @returns Promise<string|null> - Returns input value, extraBtn value, or null (cancel)
|
||||
*/
|
||||
function requestInput(title, label, val, opts = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const modal = document.getElementById('generic-input-modal');
|
||||
const inp = document.getElementById('gim-input');
|
||||
const extraBtn = document.getElementById('gim-extra-btn');
|
||||
const modalContext = 'generic-input-modal';
|
||||
|
||||
document.getElementById('gim-title').innerText = title;
|
||||
document.getElementById('gim-label').innerText = label;
|
||||
inp.value = val || '';
|
||||
|
||||
let resultToResolve = null;
|
||||
|
||||
// 1. Cleanup & Resolve
|
||||
const close = () => {
|
||||
window.removeEventListener('popstate', onPopState);
|
||||
modal.style.display = 'none';
|
||||
resolve(resultToResolve);
|
||||
};
|
||||
|
||||
// 2. Handle History Changes (Back Button)
|
||||
const onPopState = (e) => {
|
||||
// If we are here, history has ALREADY popped.
|
||||
// Just close the UI.
|
||||
close();
|
||||
};
|
||||
|
||||
// 3. Handle UI Actions (OK / Cancel)
|
||||
const commit = (v) => {
|
||||
resultToResolve = v;
|
||||
|
||||
// SAFETY CHECK: Only go back if we are still in the modal state.
|
||||
// This prevents "Double Back" if the user mashed buttons or browser lagged.
|
||||
if (history.state && history.state.context === modalContext) {
|
||||
history.back(); // This will trigger onPopState -> close()
|
||||
} else {
|
||||
// We are already out of state (shouldn't happen, but safe fallback)
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
// 4. Setup DOM
|
||||
const newOk = document.getElementById('gim-ok-btn').cloneNode(true);
|
||||
const newCancel = document.getElementById('gim-cancel-btn').cloneNode(true);
|
||||
const newExtra = extraBtn.cloneNode(true);
|
||||
|
||||
document.getElementById('gim-ok-btn').replaceWith(newOk);
|
||||
document.getElementById('gim-cancel-btn').replaceWith(newCancel);
|
||||
extraBtn.replaceWith(newExtra);
|
||||
|
||||
// StopPropagation prevents clicks from bubbling to Inspector canvas (Ghost clicks)
|
||||
newOk.onclick = (e) => { e.stopPropagation(); commit(inp.value.trim()); };
|
||||
newCancel.onclick = (e) => { e.stopPropagation(); commit(null); };
|
||||
|
||||
if(opts.extraBtn) {
|
||||
newExtra.style.display = 'block';
|
||||
newExtra.innerText = opts.extraBtn.label;
|
||||
newExtra.className = opts.extraBtn.class || 'danger';
|
||||
newExtra.onclick = (e) => { e.stopPropagation(); commit(opts.extraBtn.value); };
|
||||
} else {
|
||||
newExtra.style.display = 'none';
|
||||
}
|
||||
|
||||
modal.querySelector('.close-btn').onclick = (e) => { e.stopPropagation(); commit(null); };
|
||||
|
||||
inp.onkeydown = (e) => {
|
||||
if(e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
newOk.click();
|
||||
}
|
||||
// Let NavManager handle Escape -> history.back()
|
||||
};
|
||||
|
||||
// 5. Open & Push State
|
||||
// Use direct history.pushState to ensure it happens immediately and locally
|
||||
// (Bypassing any potential NavManager checks/delays)
|
||||
if (!history.state || history.state.context !== modalContext) {
|
||||
history.pushState({ context: modalContext }, "", "");
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', onPopState);
|
||||
modal.style.display = 'flex';
|
||||
inp.focus();
|
||||
inp.select();
|
||||
});
|
||||
}
|
||||
|
||||
// Start
|
||||
window.onload = init;
|
||||
|
||||
window.exportKiCad = () => {
|
||||
if (window.netManager) window.netManager.exportKiCad();
|
||||
if (window.netManager) window.netManager.exportKiCad();
|
||||
};
|
||||
|
||||
window.startNewNet = () => {
|
||||
if (window.inspector) window.inspector.startNewNet();
|
||||
if (window.inspector) window.inspector.startNewNet();
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue