Harden symbol editor with migration preview and pin reorder
This commit is contained in:
parent
c6578c05fe
commit
fcad4b284b
307
frontend/app.js
307
frontend/app.js
@ -40,7 +40,8 @@ const state = {
|
|||||||
historyPast: [],
|
historyPast: [],
|
||||||
historyFuture: [],
|
historyFuture: [],
|
||||||
historyLimit: 80,
|
historyLimit: 80,
|
||||||
historyRestoring: false
|
historyRestoring: false,
|
||||||
|
symbolMigrationAckHash: null
|
||||||
};
|
};
|
||||||
|
|
||||||
const el = {
|
const el = {
|
||||||
@ -78,8 +79,10 @@ const el = {
|
|||||||
symbolWidthInput: document.getElementById("symbolWidthInput"),
|
symbolWidthInput: document.getElementById("symbolWidthInput"),
|
||||||
symbolHeightInput: document.getElementById("symbolHeightInput"),
|
symbolHeightInput: document.getElementById("symbolHeightInput"),
|
||||||
addSymbolPinBtn: document.getElementById("addSymbolPinBtn"),
|
addSymbolPinBtn: document.getElementById("addSymbolPinBtn"),
|
||||||
|
previewSymbolBtn: document.getElementById("previewSymbolBtn"),
|
||||||
applySymbolBtn: document.getElementById("applySymbolBtn"),
|
applySymbolBtn: document.getElementById("applySymbolBtn"),
|
||||||
symbolValidation: document.getElementById("symbolValidation"),
|
symbolValidation: document.getElementById("symbolValidation"),
|
||||||
|
symbolMigrationPreview: document.getElementById("symbolMigrationPreview"),
|
||||||
symbolPinsList: document.getElementById("symbolPinsList"),
|
symbolPinsList: document.getElementById("symbolPinsList"),
|
||||||
pinMeta: document.getElementById("pinMeta"),
|
pinMeta: document.getElementById("pinMeta"),
|
||||||
pinNameInput: document.getElementById("pinNameInput"),
|
pinNameInput: document.getElementById("pinNameInput"),
|
||||||
@ -871,10 +874,194 @@ function symbolPinRowHtml(pin) {
|
|||||||
<select class="pinCol pinSide">${sideOptions}</select>
|
<select class="pinCol pinSide">${sideOptions}</select>
|
||||||
<input class="pinCol pinOffset" type="number" min="0" step="1" value="${Number(pin.offset ?? 0)}" />
|
<input class="pinCol pinOffset" type="number" min="0" step="1" value="${Number(pin.offset ?? 0)}" />
|
||||||
<select class="pinCol pinType">${typeOptions}</select>
|
<select class="pinCol pinType">${typeOptions}</select>
|
||||||
|
<button type="button" data-move-symbol-pin="up" title="Move pin up">Up</button>
|
||||||
|
<button type="button" data-move-symbol-pin="down" title="Move pin down">Down</button>
|
||||||
<button type="button" data-remove-symbol-pin="${escHtml(pin.name)}">Remove</button>
|
<button type="button" data-remove-symbol-pin="${escHtml(pin.name)}">Remove</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function invalidateSymbolMigrationPreview(message = "") {
|
||||||
|
state.symbolMigrationAckHash = null;
|
||||||
|
if (el.symbolMigrationPreview) {
|
||||||
|
el.symbolMigrationPreview.textContent = message;
|
||||||
|
el.symbolMigrationPreview.classList.remove("migrationPreview");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectSymbolDraft(ref) {
|
||||||
|
const inst = instanceByRef(ref);
|
||||||
|
const sym = symbolForRef(ref);
|
||||||
|
if (!inst || !sym) {
|
||||||
|
return { ok: false, message: "No symbol selected." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextCategory = String(el.symbolCategoryInput.value ?? "").trim() || String(sym.category ?? "generic");
|
||||||
|
const nextWidth = Number(el.symbolWidthInput.value);
|
||||||
|
const nextHeight = Number(el.symbolHeightInput.value);
|
||||||
|
if (!Number.isFinite(nextWidth) || !Number.isFinite(nextHeight) || nextWidth < 20 || nextHeight < 20) {
|
||||||
|
return { ok: false, message: "Symbol width/height must be >= 20." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = [...el.symbolPinsList.querySelectorAll(".symbolPinRow")];
|
||||||
|
clearSymbolRowValidation(rows);
|
||||||
|
if (!rows.length) {
|
||||||
|
return { ok: false, message: "Symbol must contain at least one pin row." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedPins = [];
|
||||||
|
for (const row of rows) {
|
||||||
|
const name = String(row.querySelector(".pinName")?.value ?? "").trim();
|
||||||
|
const number = String(row.querySelector(".pinNumber")?.value ?? "").trim();
|
||||||
|
const side = String(row.querySelector(".pinSide")?.value ?? "");
|
||||||
|
const offset = Number(row.querySelector(".pinOffset")?.value ?? 0);
|
||||||
|
const type = String(row.querySelector(".pinType")?.value ?? "");
|
||||||
|
if (!name || !number || !PIN_SIDES.includes(side) || !PIN_TYPES.includes(type) || !Number.isFinite(offset) || offset < 0) {
|
||||||
|
row.classList.add("invalidRow");
|
||||||
|
return { ok: false, message: "Each pin row needs name, number, valid side/type, and offset >= 0." };
|
||||||
|
}
|
||||||
|
parsedPins.push({
|
||||||
|
oldName: row.getAttribute("data-old-pin") ?? name,
|
||||||
|
pin: { name, number, side, offset: Math.round(offset), type }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameCounts = new Map();
|
||||||
|
const numberCounts = new Map();
|
||||||
|
for (const p of parsedPins) {
|
||||||
|
nameCounts.set(p.pin.name, (nameCounts.get(p.pin.name) ?? 0) + 1);
|
||||||
|
numberCounts.set(p.pin.number, (numberCounts.get(p.pin.number) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
let dupName = null;
|
||||||
|
let dupNumber = null;
|
||||||
|
for (const [name, count] of nameCounts) {
|
||||||
|
if (count > 1) {
|
||||||
|
dupName = name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [num, count] of numberCounts) {
|
||||||
|
if (count > 1) {
|
||||||
|
dupNumber = num;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dupName || dupNumber) {
|
||||||
|
for (const row of rows) {
|
||||||
|
const name = String(row.querySelector(".pinName")?.value ?? "").trim();
|
||||||
|
const num = String(row.querySelector(".pinNumber")?.value ?? "").trim();
|
||||||
|
if ((dupName && name === dupName) || (dupNumber && num === dupNumber)) {
|
||||||
|
row.classList.add("invalidRow");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dupName) {
|
||||||
|
return { ok: false, message: `Duplicate pin name '${dupName}' detected.` };
|
||||||
|
}
|
||||||
|
return { ok: false, message: `Duplicate pin number '${dupNumber}' detected.` };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
inst,
|
||||||
|
sym,
|
||||||
|
parsedPins,
|
||||||
|
nextCategory,
|
||||||
|
nextWidth: Math.round(nextWidth),
|
||||||
|
nextHeight: Math.round(nextHeight)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSymbolMigrationPlan(symbolId, beforePins, parsedPins) {
|
||||||
|
const beforeNames = new Set(beforePins.map((p) => p.name));
|
||||||
|
const afterNames = new Set(parsedPins.map((p) => p.pin.name));
|
||||||
|
const renameMap = new Map();
|
||||||
|
for (const entry of parsedPins) {
|
||||||
|
if (entry.oldName && beforeNames.has(entry.oldName) && entry.oldName !== entry.pin.name) {
|
||||||
|
renameMap.set(entry.oldName, entry.pin.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removedPins = beforePins
|
||||||
|
.map((p) => p.name)
|
||||||
|
.filter((name) => !afterNames.has(name) && !renameMap.has(name))
|
||||||
|
.sort((a, b) => a.localeCompare(b));
|
||||||
|
const renamedPins = [...renameMap.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
||||||
|
const refs = new Set((state.model.instances ?? []).filter((i) => i.symbol === symbolId).map((i) => i.ref));
|
||||||
|
|
||||||
|
let droppedNodes = 0;
|
||||||
|
const touchedNets = new Set();
|
||||||
|
for (const net of state.model.nets ?? []) {
|
||||||
|
for (const node of net.nodes ?? []) {
|
||||||
|
if (!refs.has(node.ref)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const migratedPin = renameMap.get(node.pin) ?? node.pin;
|
||||||
|
if (!afterNames.has(migratedPin)) {
|
||||||
|
droppedNodes += 1;
|
||||||
|
touchedNets.add(net.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let removedPinUiEntries = 0;
|
||||||
|
for (const inst of state.model.instances ?? []) {
|
||||||
|
if (!refs.has(inst.ref)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const pinUi = inst.properties?.pin_ui;
|
||||||
|
if (!pinUi || typeof pinUi !== "object" || Array.isArray(pinUi)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(pinUi)) {
|
||||||
|
const migratedPin = renameMap.get(key) ?? key;
|
||||||
|
if (!afterNames.has(migratedPin)) {
|
||||||
|
removedPinUiEntries += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasDestructive = removedPins.length > 0 || droppedNodes > 0 || removedPinUiEntries > 0;
|
||||||
|
const hash = JSON.stringify({
|
||||||
|
symbolId,
|
||||||
|
renamedPins,
|
||||||
|
removedPins,
|
||||||
|
droppedNodes,
|
||||||
|
touchedNets: [...touchedNets].sort(),
|
||||||
|
removedPinUiEntries
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
hash,
|
||||||
|
hasDestructive,
|
||||||
|
renamedPins,
|
||||||
|
removedPins,
|
||||||
|
droppedNodes,
|
||||||
|
touchedNets: [...touchedNets].sort(),
|
||||||
|
removedPinUiEntries
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSymbolMigrationPlan(plan) {
|
||||||
|
if (!el.symbolMigrationPreview) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lines = [];
|
||||||
|
lines.push(`Renamed pins: ${plan.renamedPins.length}`);
|
||||||
|
lines.push(`Removed pins: ${plan.removedPins.length}`);
|
||||||
|
lines.push(`Dropped net nodes: ${plan.droppedNodes}`);
|
||||||
|
lines.push(`Removed pin-ui mappings: ${plan.removedPinUiEntries}`);
|
||||||
|
if (plan.removedPins.length) {
|
||||||
|
lines.push(`Removed: ${plan.removedPins.join(", ")}`);
|
||||||
|
}
|
||||||
|
if (plan.touchedNets.length) {
|
||||||
|
const sample = plan.touchedNets.slice(0, 8).join(", ");
|
||||||
|
const suffix = plan.touchedNets.length > 8 ? ` (+${plan.touchedNets.length - 8} more)` : "";
|
||||||
|
lines.push(`Affected nets: ${sample}${suffix}`);
|
||||||
|
}
|
||||||
|
lines.push(plan.hasDestructive ? "Destructive changes detected. Preview must be acknowledged before apply." : "No destructive migration detected.");
|
||||||
|
el.symbolMigrationPreview.innerHTML = `<div>${lines.map((line) => escHtml(line)).join("<br/>")}</div>`;
|
||||||
|
el.symbolMigrationPreview.classList.add("migrationPreview");
|
||||||
|
}
|
||||||
|
|
||||||
function renderSymbolEditorForRef(ref) {
|
function renderSymbolEditorForRef(ref) {
|
||||||
const inst = instanceByRef(ref);
|
const inst = instanceByRef(ref);
|
||||||
const sym = symbolForRef(ref);
|
const sym = symbolForRef(ref);
|
||||||
@ -888,6 +1075,7 @@ function renderSymbolEditorForRef(ref) {
|
|||||||
el.symbolHeightInput.value = String(Number(sym.body?.height ?? 80));
|
el.symbolHeightInput.value = String(Number(sym.body?.height ?? 80));
|
||||||
el.symbolValidation.textContent = "";
|
el.symbolValidation.textContent = "";
|
||||||
el.symbolValidation.classList.remove("symbolValidationError");
|
el.symbolValidation.classList.remove("symbolValidationError");
|
||||||
|
invalidateSymbolMigrationPreview("");
|
||||||
el.symbolPinsList.innerHTML = (sym.pins ?? []).map((pin) => symbolPinRowHtml(pin)).join("");
|
el.symbolPinsList.innerHTML = (sym.pins ?? []).map((pin) => symbolPinRowHtml(pin)).join("");
|
||||||
el.symbolEditor.classList.remove("hidden");
|
el.symbolEditor.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
@ -2318,9 +2506,26 @@ function setupEvents() {
|
|||||||
type: "passive"
|
type: "passive"
|
||||||
};
|
};
|
||||||
el.symbolPinsList.insertAdjacentHTML("beforeend", symbolPinRowHtml(row));
|
el.symbolPinsList.insertAdjacentHTML("beforeend", symbolPinRowHtml(row));
|
||||||
|
invalidateSymbolMigrationPreview("Pin rows changed. Preview migration again before apply.");
|
||||||
});
|
});
|
||||||
|
|
||||||
el.symbolPinsList.addEventListener("click", (evt) => {
|
el.symbolPinsList.addEventListener("click", (evt) => {
|
||||||
|
const moveBtn = evt.target.closest("[data-move-symbol-pin]");
|
||||||
|
if (moveBtn) {
|
||||||
|
const row = moveBtn.closest(".symbolPinRow");
|
||||||
|
if (!row) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dir = moveBtn.getAttribute("data-move-symbol-pin");
|
||||||
|
if (dir === "up" && row.previousElementSibling) {
|
||||||
|
row.parentElement.insertBefore(row, row.previousElementSibling);
|
||||||
|
} else if (dir === "down" && row.nextElementSibling) {
|
||||||
|
row.parentElement.insertBefore(row.nextElementSibling, row);
|
||||||
|
}
|
||||||
|
invalidateSymbolMigrationPreview("Pin order changed. Preview migration again before apply.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const btn = evt.target.closest("[data-remove-symbol-pin]");
|
const btn = evt.target.closest("[data-remove-symbol-pin]");
|
||||||
if (!btn) {
|
if (!btn) {
|
||||||
return;
|
return;
|
||||||
@ -2328,6 +2533,7 @@ function setupEvents() {
|
|||||||
const row = btn.closest(".symbolPinRow");
|
const row = btn.closest(".symbolPinRow");
|
||||||
if (row) {
|
if (row) {
|
||||||
row.remove();
|
row.remove();
|
||||||
|
invalidateSymbolMigrationPreview("Pin rows changed. Preview migration again before apply.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -2340,76 +2546,50 @@ function setupEvents() {
|
|||||||
el.symbolValidation.textContent = "";
|
el.symbolValidation.textContent = "";
|
||||||
el.symbolValidation.classList.remove("symbolValidationError");
|
el.symbolValidation.classList.remove("symbolValidationError");
|
||||||
}
|
}
|
||||||
|
invalidateSymbolMigrationPreview("Symbol changes pending. Preview migration before apply.");
|
||||||
|
});
|
||||||
|
|
||||||
|
el.previewSymbolBtn.addEventListener("click", () => {
|
||||||
|
if (!state.selectedRef) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const draft = collectSymbolDraft(state.selectedRef);
|
||||||
|
if (!draft.ok) {
|
||||||
|
el.jsonFeedback.textContent = draft.message;
|
||||||
|
el.symbolValidation.textContent = draft.message;
|
||||||
|
el.symbolValidation.classList.add("symbolValidationError");
|
||||||
|
invalidateSymbolMigrationPreview("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const plan = buildSymbolMigrationPlan(draft.inst.symbol, draft.sym.pins ?? [], draft.parsedPins);
|
||||||
|
renderSymbolMigrationPlan(plan);
|
||||||
|
state.symbolMigrationAckHash = plan.hash;
|
||||||
|
el.jsonFeedback.textContent = plan.hasDestructive
|
||||||
|
? "Migration preview acknowledged. Apply is now enabled for destructive symbol edits."
|
||||||
|
: "Preview complete. No destructive migration detected.";
|
||||||
});
|
});
|
||||||
|
|
||||||
el.applySymbolBtn.addEventListener("click", () => {
|
el.applySymbolBtn.addEventListener("click", () => {
|
||||||
if (!state.selectedRef) {
|
if (!state.selectedRef) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const inst = instanceByRef(state.selectedRef);
|
const draft = collectSymbolDraft(state.selectedRef);
|
||||||
const sym = symbolForRef(state.selectedRef);
|
if (!draft.ok) {
|
||||||
if (!inst || !sym) {
|
el.jsonFeedback.textContent = draft.message;
|
||||||
return;
|
el.symbolValidation.textContent = draft.message;
|
||||||
}
|
|
||||||
const nextCategory = String(el.symbolCategoryInput.value ?? "").trim() || String(sym.category ?? "generic");
|
|
||||||
const nextWidth = Number(el.symbolWidthInput.value);
|
|
||||||
const nextHeight = Number(el.symbolHeightInput.value);
|
|
||||||
if (!Number.isFinite(nextWidth) || !Number.isFinite(nextHeight) || nextWidth < 20 || nextHeight < 20) {
|
|
||||||
el.jsonFeedback.textContent = "Symbol width/height must be >= 20.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = [...el.symbolPinsList.querySelectorAll(".symbolPinRow")];
|
|
||||||
clearSymbolRowValidation(rows);
|
|
||||||
el.symbolValidation.textContent = "";
|
|
||||||
el.symbolValidation.classList.remove("symbolValidationError");
|
|
||||||
if (!rows.length) {
|
|
||||||
el.jsonFeedback.textContent = "Symbol must have at least one pin.";
|
|
||||||
el.symbolValidation.textContent = "Symbol must contain at least one pin row.";
|
|
||||||
el.symbolValidation.classList.add("symbolValidationError");
|
el.symbolValidation.classList.add("symbolValidationError");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const { inst, sym, parsedPins, nextCategory, nextWidth, nextHeight } = draft;
|
||||||
const parsedPins = [];
|
const plan = buildSymbolMigrationPlan(inst.symbol, sym.pins ?? [], parsedPins);
|
||||||
const rowErrors = [];
|
if (plan.hasDestructive && state.symbolMigrationAckHash !== plan.hash) {
|
||||||
for (const row of rows) {
|
renderSymbolMigrationPlan(plan);
|
||||||
const name = String(row.querySelector(".pinName")?.value ?? "").trim();
|
el.jsonFeedback.textContent = "Destructive symbol edit detected. Click 'Preview Migration' before applying.";
|
||||||
const number = String(row.querySelector(".pinNumber")?.value ?? "").trim();
|
el.symbolValidation.textContent = "Preview migration is required for pin removals or dropped net mappings.";
|
||||||
const side = String(row.querySelector(".pinSide")?.value ?? "");
|
|
||||||
const offset = Number(row.querySelector(".pinOffset")?.value ?? 0);
|
|
||||||
const type = String(row.querySelector(".pinType")?.value ?? "");
|
|
||||||
if (!name || !number || !PIN_SIDES.includes(side) || !PIN_TYPES.includes(type) || !Number.isFinite(offset) || offset < 0) {
|
|
||||||
row.classList.add("invalidRow");
|
|
||||||
rowErrors.push("Each pin row needs name, number, valid side/type, and offset >= 0.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
parsedPins.push({
|
|
||||||
oldName: row.getAttribute("data-old-pin") ?? name,
|
|
||||||
pin: { name, number, side, offset: Math.round(offset), type }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rowErrors.length) {
|
|
||||||
el.jsonFeedback.textContent = "Fix invalid symbol pin rows before applying.";
|
|
||||||
el.symbolValidation.textContent = rowErrors[0];
|
|
||||||
el.symbolValidation.classList.add("symbolValidationError");
|
el.symbolValidation.classList.add("symbolValidationError");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const unique = new Set(parsedPins.map((p) => p.pin.name));
|
|
||||||
if (unique.size !== parsedPins.length) {
|
|
||||||
el.jsonFeedback.textContent = "Duplicate pin names are not allowed.";
|
|
||||||
el.symbolValidation.textContent = "Duplicate pin names detected. Each pin name must be unique.";
|
|
||||||
el.symbolValidation.classList.add("symbolValidationError");
|
|
||||||
for (const row of rows) {
|
|
||||||
const name = String(row.querySelector(".pinName")?.value ?? "").trim();
|
|
||||||
if (name && parsedPins.filter((p) => p.pin.name === name).length > 1) {
|
|
||||||
row.classList.add("invalidRow");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pushHistory("symbol-edit");
|
pushHistory("symbol-edit");
|
||||||
const beforePins = new Set((sym.pins ?? []).map((p) => p.name));
|
const beforePins = new Set((sym.pins ?? []).map((p) => p.name));
|
||||||
for (const entry of parsedPins) {
|
for (const entry of parsedPins) {
|
||||||
@ -2420,8 +2600,8 @@ function setupEvents() {
|
|||||||
sym.category = nextCategory;
|
sym.category = nextCategory;
|
||||||
sym.body = {
|
sym.body = {
|
||||||
...(sym.body ?? {}),
|
...(sym.body ?? {}),
|
||||||
width: Math.round(nextWidth),
|
width: nextWidth,
|
||||||
height: Math.round(nextHeight)
|
height: nextHeight
|
||||||
};
|
};
|
||||||
sym.pins = parsedPins.map((p) => p.pin);
|
sym.pins = parsedPins.map((p) => p.pin);
|
||||||
const allowedPins = new Set(sym.pins.map((p) => p.name));
|
const allowedPins = new Set(sym.pins.map((p) => p.name));
|
||||||
@ -2450,6 +2630,7 @@ function setupEvents() {
|
|||||||
}
|
}
|
||||||
el.symbolValidation.textContent = "";
|
el.symbolValidation.textContent = "";
|
||||||
el.symbolValidation.classList.remove("symbolValidationError");
|
el.symbolValidation.classList.remove("symbolValidationError");
|
||||||
|
invalidateSymbolMigrationPreview("");
|
||||||
const removedPinCount = [...beforePins].filter((p) => !allowedPins.has(p)).length;
|
const removedPinCount = [...beforePins].filter((p) => !allowedPins.has(p)).length;
|
||||||
el.jsonFeedback.textContent = removedPinCount
|
el.jsonFeedback.textContent = removedPinCount
|
||||||
? `Updated symbol ${inst.symbol}. Removed ${removedPinCount} pin mappings from nets/UI metadata.`
|
? `Updated symbol ${inst.symbol}. Removed ${removedPinCount} pin mappings from nets/UI metadata.`
|
||||||
@ -2457,6 +2638,12 @@ function setupEvents() {
|
|||||||
queueCompile(true, "symbol-edit");
|
queueCompile(true, "symbol-edit");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
[el.symbolCategoryInput, el.symbolWidthInput, el.symbolHeightInput].forEach((input) => {
|
||||||
|
input?.addEventListener("input", () => {
|
||||||
|
invalidateSymbolMigrationPreview("Symbol changes pending. Preview migration before apply.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
el.zoomInBtn.addEventListener("click", () => {
|
el.zoomInBtn.addEventListener("click", () => {
|
||||||
state.scale = Math.min(4, state.scale + 0.1);
|
state.scale = Math.min(4, state.scale + 0.1);
|
||||||
state.userAdjustedView = true;
|
state.userAdjustedView = true;
|
||||||
|
|||||||
@ -111,9 +111,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="editorActions">
|
<div class="editorActions">
|
||||||
<button id="addSymbolPinBtn">Add Pin</button>
|
<button id="addSymbolPinBtn">Add Pin</button>
|
||||||
|
<button id="previewSymbolBtn">Preview Migration</button>
|
||||||
<button id="applySymbolBtn">Apply Symbol</button>
|
<button id="applySymbolBtn">Apply Symbol</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="symbolValidation" class="hintText"></div>
|
<div id="symbolValidation" class="hintText"></div>
|
||||||
|
<div id="symbolMigrationPreview" class="hintText"></div>
|
||||||
<div id="symbolPinsList" class="miniList"></div>
|
<div id="symbolPinsList" class="miniList"></div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@ -284,7 +284,7 @@ textarea {
|
|||||||
|
|
||||||
.symbolPinRow {
|
.symbolPinRow {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 0.9fr 0.9fr 0.8fr 1fr auto;
|
grid-template-columns: 1fr 0.9fr 0.9fr 0.8fr 1fr auto auto auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,6 +303,16 @@ textarea {
|
|||||||
font-size: 0.76rem;
|
font-size: 0.76rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.migrationPreview {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #f8fafc;
|
||||||
|
margin-top: 6px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
.canvasTools {
|
.canvasTools {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user