Add inspector quick actions for duplicate/delete/isolate

This commit is contained in:
Rbanh 2026-02-16 22:05:06 -05:00
parent f938c6dcbc
commit b7ba158880
2 changed files with 98 additions and 0 deletions

View File

@ -63,6 +63,9 @@ const el = {
lockedInput: document.getElementById("lockedInput"), lockedInput: document.getElementById("lockedInput"),
rotateSelectedBtn: document.getElementById("rotateSelectedBtn"), rotateSelectedBtn: document.getElementById("rotateSelectedBtn"),
updatePlacementBtn: document.getElementById("updatePlacementBtn"), updatePlacementBtn: document.getElementById("updatePlacementBtn"),
duplicateComponentBtn: document.getElementById("duplicateComponentBtn"),
deleteComponentBtn: document.getElementById("deleteComponentBtn"),
isolateSelectedComponentBtn: document.getElementById("isolateSelectedComponentBtn"),
symbolMeta: document.getElementById("symbolMeta"), symbolMeta: document.getElementById("symbolMeta"),
symbolCategoryInput: document.getElementById("symbolCategoryInput"), symbolCategoryInput: document.getElementById("symbolCategoryInput"),
symbolWidthInput: document.getElementById("symbolWidthInput"), symbolWidthInput: document.getElementById("symbolWidthInput"),
@ -87,6 +90,7 @@ const el = {
netNameInput: document.getElementById("netNameInput"), netNameInput: document.getElementById("netNameInput"),
netClassInput: document.getElementById("netClassInput"), netClassInput: document.getElementById("netClassInput"),
updateNetBtn: document.getElementById("updateNetBtn"), updateNetBtn: document.getElementById("updateNetBtn"),
isolateSelectedNetBtn: document.getElementById("isolateSelectedNetBtn"),
netNodeRefInput: document.getElementById("netNodeRefInput"), netNodeRefInput: document.getElementById("netNodeRefInput"),
netNodePinInput: document.getElementById("netNodePinInput"), netNodePinInput: document.getElementById("netNodePinInput"),
addNetNodeBtn: document.getElementById("addNetNodeBtn"), addNetNodeBtn: document.getElementById("addNetNodeBtn"),
@ -261,6 +265,19 @@ function normalizeNetName(raw) {
.replace(/\s+/g, "_"); .replace(/\s+/g, "_");
} }
function nextRefLike(baseRef) {
const base = normalizeRef(baseRef || "X1");
const m = /^([A-Za-z_]+)(\d+)?$/i.exec(base);
const prefix = m?.[1] ?? `${base}_`;
let n = Number(m?.[2] ?? 1);
let candidate = `${prefix}${n}`;
while (instanceByRef(candidate)) {
n += 1;
candidate = `${prefix}${n}`;
}
return candidate;
}
function escHtml(text) { function escHtml(text) {
return String(text ?? "") return String(text ?? "")
.replaceAll("&", "&") .replaceAll("&", "&")
@ -890,6 +907,11 @@ function renderNetEditor() {
} }
function renderSelected() { function renderSelected() {
el.duplicateComponentBtn.disabled = true;
el.deleteComponentBtn.disabled = true;
el.isolateSelectedComponentBtn.disabled = true;
el.isolateSelectedNetBtn.disabled = true;
if (!state.model) { if (!state.model) {
el.selectedSummary.textContent = "Click a component, net, or pin to inspect it."; el.selectedSummary.textContent = "Click a component, net, or pin to inspect it.";
el.componentEditor.classList.add("hidden"); el.componentEditor.classList.add("hidden");
@ -931,6 +953,9 @@ function renderSelected() {
el.instRefInput.value = inst.ref; el.instRefInput.value = inst.ref;
el.instValueInput.value = String(inst.properties?.value ?? ""); el.instValueInput.value = String(inst.properties?.value ?? "");
el.instNotesInput.value = String(inst.properties?.notes ?? ""); el.instNotesInput.value = String(inst.properties?.notes ?? "");
el.duplicateComponentBtn.disabled = false;
el.deleteComponentBtn.disabled = false;
el.isolateSelectedComponentBtn.disabled = false;
renderSymbolEditorForRef(inst.ref); renderSymbolEditorForRef(inst.ref);
el.pinEditor.classList.add("hidden"); el.pinEditor.classList.add("hidden");
el.netEditor.classList.add("hidden"); el.netEditor.classList.add("hidden");
@ -941,6 +966,7 @@ function renderSelected() {
const net = state.model.nets.find((n) => n.name === state.selectedNet); const net = state.model.nets.find((n) => n.name === state.selectedNet);
if (net) { if (net) {
el.selectedSummary.textContent = `${net.name} (${net.class})\nNodes: ${net.nodes.map((n) => `${n.ref}.${n.pin}`).join(", ")}`; el.selectedSummary.textContent = `${net.name} (${net.class})\nNodes: ${net.nodes.map((n) => `${n.ref}.${n.pin}`).join(", ")}`;
el.isolateSelectedNetBtn.disabled = false;
el.componentEditor.classList.add("hidden"); el.componentEditor.classList.add("hidden");
el.symbolEditor.classList.add("hidden"); el.symbolEditor.classList.add("hidden");
el.pinEditor.classList.add("hidden"); el.pinEditor.classList.add("hidden");
@ -1915,6 +1941,63 @@ function setupEvents() {
queueCompile(true, "rotate"); queueCompile(true, "rotate");
}); });
el.duplicateComponentBtn.addEventListener("click", () => {
if (state.selectedRefs.length !== 1 || !state.selectedRef || !state.model) {
return;
}
const inst = instanceByRef(state.selectedRef);
if (!inst) {
return;
}
pushHistory("duplicate-component");
const nextRef = nextRefLike(inst.ref);
const next = clone(inst);
next.ref = nextRef;
next.placement = {
...next.placement,
x: toGrid(Number(next.placement.x ?? 0) + GRID * 2),
y: toGrid(Number(next.placement.y ?? 0) + GRID * 2),
locked: false
};
state.model.instances.push(next);
selectSingleRef(nextRef);
state.selectedNet = null;
state.selectedPin = null;
state.isolateNet = false;
el.jsonFeedback.textContent = `Duplicated ${inst.ref} as ${nextRef}.`;
queueCompile(true, "duplicate-component");
});
el.deleteComponentBtn.addEventListener("click", () => {
if (state.selectedRefs.length !== 1 || !state.selectedRef || !state.model) {
return;
}
const ref = state.selectedRef;
pushHistory("delete-component");
state.model.instances = state.model.instances.filter((i) => i.ref !== ref);
for (const net of state.model.nets ?? []) {
net.nodes = (net.nodes ?? []).filter((n) => n.ref !== ref);
}
state.model.nets = (state.model.nets ?? []).filter((n) => (n.nodes ?? []).length >= 2);
setSelectedRefs([]);
state.selectedNet = null;
state.selectedPin = null;
state.isolateComponent = false;
state.isolateNet = false;
el.jsonFeedback.textContent = `Deleted component ${ref}.`;
queueCompile(true, "delete-component");
});
el.isolateSelectedComponentBtn.addEventListener("click", () => {
if (!state.selectedRef) {
return;
}
state.isolateComponent = true;
state.isolateNet = false;
state.selectedNet = null;
renderAll();
});
el.showPinNetLabelInput.addEventListener("change", () => { el.showPinNetLabelInput.addEventListener("change", () => {
if (!state.selectedPin) { if (!state.selectedPin) {
return; return;
@ -2054,6 +2137,17 @@ function setupEvents() {
queueCompile(true, "net-edit"); queueCompile(true, "net-edit");
}); });
el.isolateSelectedNetBtn.addEventListener("click", () => {
if (!state.selectedNet) {
return;
}
state.isolateNet = true;
state.isolateComponent = false;
setSelectedRefs([]);
state.selectedPin = null;
renderAll();
});
el.addNetNodeBtn.addEventListener("click", () => { el.addNetNodeBtn.addEventListener("click", () => {
if (!state.selectedNet) { if (!state.selectedNet) {
return; return;

View File

@ -92,6 +92,9 @@
<div class="editorActions"> <div class="editorActions">
<button id="rotateSelectedBtn">Rotate +90</button> <button id="rotateSelectedBtn">Rotate +90</button>
<button id="updatePlacementBtn">Apply Component</button> <button id="updatePlacementBtn">Apply Component</button>
<button id="duplicateComponentBtn">Duplicate</button>
<button id="deleteComponentBtn">Delete</button>
<button id="isolateSelectedComponentBtn">Isolate</button>
</div> </div>
</div> </div>
<div id="symbolEditor" class="editorCard hidden"> <div id="symbolEditor" class="editorCard hidden">
@ -180,6 +183,7 @@
</div> </div>
<div class="editorActions"> <div class="editorActions">
<button id="updateNetBtn">Apply Net</button> <button id="updateNetBtn">Apply Net</button>
<button id="isolateSelectedNetBtn">Isolate</button>
</div> </div>
<div class="editorGrid"> <div class="editorGrid">
<label>Node Ref <input id="netNodeRefInput" type="text" placeholder="U1" /></label> <label>Node Ref <input id="netNodeRefInput" type="text" placeholder="U1" /></label>