Virtualize instance and net lists for large schematic performance

This commit is contained in:
Rbanh 2026-02-17 00:44:47 -05:00
parent 3cb6feeb15
commit 559ff51013
2 changed files with 53 additions and 10 deletions

View File

@ -4,6 +4,8 @@ const SCHEMA_URL = "/schemeta.schema.json";
const NET_CLASSES = ["power", "ground", "signal", "analog", "differential", "clock", "bus"]; const NET_CLASSES = ["power", "ground", "signal", "analog", "differential", "clock", "bus"];
const PIN_SIDES = ["left", "right", "top", "bottom"]; const PIN_SIDES = ["left", "right", "top", "bottom"];
const PIN_TYPES = ["power_in", "power_out", "input", "output", "bidirectional", "passive", "analog", "ground"]; const PIN_TYPES = ["power_in", "power_out", "input", "output", "bidirectional", "passive", "analog", "ground"];
const LIST_ROW_HEIGHT = 36;
const LIST_OVERSCAN_ROWS = 8;
const state = { const state = {
model: null, model: null,
@ -721,6 +723,26 @@ function resolveLabelCollisions(svg) {
} }
} }
function renderVirtualList(listEl, allItems, renderRow, rowHeight = LIST_ROW_HEIGHT) {
const items = Array.isArray(allItems) ? allItems : [];
if (!items.length) {
listEl.innerHTML = "";
return;
}
const viewportHeight = Math.max(rowHeight * 4, listEl.clientHeight || 230);
const scrollTop = listEl.scrollTop;
const total = items.length;
const visibleRows = Math.ceil(viewportHeight / rowHeight);
const start = Math.max(0, Math.floor(scrollTop / rowHeight) - LIST_OVERSCAN_ROWS);
const end = Math.min(total, start + visibleRows + LIST_OVERSCAN_ROWS * 2);
const topPad = start * rowHeight;
const bottomPad = (total - end) * rowHeight;
const rows = items.slice(start, end).map(renderRow).join("");
listEl.innerHTML = `<li class="listSpacer" style="height:${topPad}px"></li>${rows}<li class="listSpacer" style="height:${bottomPad}px"></li>`;
}
function renderInstances() { function renderInstances() {
if (!state.model) { if (!state.model) {
el.instanceList.innerHTML = ""; el.instanceList.innerHTML = "";
@ -730,12 +752,15 @@ function renderInstances() {
const q = el.instanceFilter.value.trim().toLowerCase(); const q = el.instanceFilter.value.trim().toLowerCase();
const items = state.model.instances.filter((i) => i.ref.toLowerCase().includes(q)); const items = state.model.instances.filter((i) => i.ref.toLowerCase().includes(q));
el.instanceList.innerHTML = items renderVirtualList(
.map((inst) => { el.instanceList,
items,
(inst) => {
const cls = state.selectedRefs.includes(inst.ref) ? "active" : ""; const cls = state.selectedRefs.includes(inst.ref) ? "active" : "";
return `<li class="${cls}" data-ref-item="${inst.ref}">${inst.ref} · ${inst.symbol}</li>`; return `<li class="${cls}" data-ref-item="${inst.ref}">${inst.ref} · ${inst.symbol}</li>`;
}) },
.join(""); LIST_ROW_HEIGHT
);
} }
function renderNets() { function renderNets() {
@ -747,12 +772,15 @@ function renderNets() {
const q = el.netFilter.value.trim().toLowerCase(); const q = el.netFilter.value.trim().toLowerCase();
const items = state.model.nets.filter((n) => n.name.toLowerCase().includes(q)); const items = state.model.nets.filter((n) => n.name.toLowerCase().includes(q));
el.netList.innerHTML = items renderVirtualList(
.map((net) => { el.netList,
items,
(net) => {
const cls = net.name === state.selectedNet ? "active" : ""; const cls = net.name === state.selectedNet ? "active" : "";
return `<li class="${cls}" data-net-item="${net.name}">${net.name} <small>(${net.class})</small></li>`; return `<li class="${cls}" data-net-item="${net.name}">${net.name} <small>(${net.class})</small></li>`;
}) },
.join(""); LIST_ROW_HEIGHT
);
} }
function netByName(name) { function netByName(name) {
@ -1843,8 +1871,16 @@ async function loadSample() {
} }
function setupEvents() { function setupEvents() {
el.instanceFilter.addEventListener("input", renderInstances); el.instanceFilter.addEventListener("input", () => {
el.netFilter.addEventListener("input", renderNets); el.instanceList.scrollTop = 0;
renderInstances();
});
el.netFilter.addEventListener("input", () => {
el.netList.scrollTop = 0;
renderNets();
});
el.instanceList.addEventListener("scroll", renderInstances, { passive: true });
el.netList.addEventListener("scroll", renderNets, { passive: true });
el.instanceList.addEventListener("click", (evt) => { el.instanceList.addEventListener("click", (evt) => {
const item = evt.target.closest("[data-ref-item]"); const item = evt.target.closest("[data-ref-item]");

View File

@ -182,6 +182,13 @@ textarea {
background: var(--accent-soft); background: var(--accent-soft);
} }
.list li.listSpacer {
padding: 0;
border: none;
cursor: default;
background: transparent;
}
.card { .card {
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: 8px;