Add UI-native component/net creation workflows and QA coverage

This commit is contained in:
Rbanh 2026-02-18 22:09:42 -05:00
parent 2bda088223
commit 486092e884
10 changed files with 123 additions and 0 deletions

View File

@ -53,6 +53,12 @@ const el = {
netList: document.getElementById("netList"), netList: document.getElementById("netList"),
instanceFilter: document.getElementById("instanceFilter"), instanceFilter: document.getElementById("instanceFilter"),
netFilter: document.getElementById("netFilter"), netFilter: document.getElementById("netFilter"),
newComponentRefInput: document.getElementById("newComponentRefInput"),
newComponentTypeSelect: document.getElementById("newComponentTypeSelect"),
addComponentBtn: document.getElementById("addComponentBtn"),
newQuickNetNameInput: document.getElementById("newQuickNetNameInput"),
newQuickNetClassSelect: document.getElementById("newQuickNetClassSelect"),
addQuickNetBtn: document.getElementById("addQuickNetBtn"),
canvasViewport: document.getElementById("canvasViewport"), canvasViewport: document.getElementById("canvasViewport"),
canvasInner: document.getElementById("canvasInner"), canvasInner: document.getElementById("canvasInner"),
selectionBox: document.getElementById("selectionBox"), selectionBox: document.getElementById("selectionBox"),
@ -295,6 +301,17 @@ function nextRefLike(baseRef) {
return candidate; return candidate;
} }
function defaultRefSeedForPart(partName) {
const part = String(partName ?? "").toLowerCase();
if (part === "resistor") return "R1";
if (part === "capacitor") return "C1";
if (part === "inductor") return "L1";
if (part === "diode" || part === "led") return "D1";
if (part === "connector") return "J1";
if (part === "generic") return "X1";
return "U1";
}
function escHtml(text) { function escHtml(text) {
return String(text ?? "") return String(text ?? "")
.replaceAll("&", "&") .replaceAll("&", "&")
@ -2257,6 +2274,65 @@ function setupEvents() {
}); });
el.instanceList.addEventListener("scroll", renderInstances, { passive: true }); el.instanceList.addEventListener("scroll", renderInstances, { passive: true });
el.netList.addEventListener("scroll", renderNets, { passive: true }); el.netList.addEventListener("scroll", renderNets, { passive: true });
if (el.newComponentRefInput && el.newComponentTypeSelect) {
const syncRefPlaceholder = () => {
el.newComponentRefInput.placeholder = defaultRefSeedForPart(el.newComponentTypeSelect.value);
};
syncRefPlaceholder();
el.newComponentTypeSelect.addEventListener("change", syncRefPlaceholder);
}
el.addComponentBtn?.addEventListener("click", async () => {
if (!state.model) {
return;
}
const part = String(el.newComponentTypeSelect?.value ?? "generic").toLowerCase();
const rawRef = normalizeRef(el.newComponentRefInput?.value ?? "");
const ref = rawRef || nextRefLike(defaultRefSeedForPart(part));
if (instanceByRef(ref)) {
el.jsonFeedback.textContent = `Component '${ref}' already exists.`;
return;
}
pushHistory("add-component");
state.model.instances.push({
ref,
part,
properties: {},
placement: { x: null, y: null, rotation: 0, locked: false }
});
el.newComponentRefInput.value = "";
setSelectedRefs([ref]);
state.selectedNet = null;
state.selectedPin = null;
await compileModel(state.model, { keepView: true, source: "add-component" });
el.jsonFeedback.textContent = `Added component ${ref} (${part}).`;
});
el.addQuickNetBtn?.addEventListener("click", async () => {
if (!state.model) {
return;
}
const rawName = normalizeNetName(el.newQuickNetNameInput?.value ?? "");
const name = rawName || nextAutoNetName();
const netClass = NET_CLASSES.includes(el.newQuickNetClassSelect?.value ?? "") ? el.newQuickNetClassSelect.value : "signal";
if (netByName(name)) {
el.jsonFeedback.textContent = `Net '${name}' already exists.`;
return;
}
pushHistory("add-net");
const nodes = state.selectedPin ? [{ ref: state.selectedPin.ref, pin: state.selectedPin.pin }] : [];
state.model.nets.push({ name, class: netClass, nodes });
el.newQuickNetNameInput.value = "";
state.selectedNet = name;
await compileModel(state.model, { keepView: true, source: "add-net" });
el.jsonFeedback.textContent = nodes.length
? `Added net ${name} (${netClass}) and connected selected pin.`
: `Added net ${name} (${netClass}).`;
});
[el.componentSection, el.symbolSection, el.pinSection, el.netSection].forEach((section) => { [el.componentSection, el.symbolSection, el.pinSection, el.netSection].forEach((section) => {
if (!section) { if (!section) {
return; return;

View File

@ -41,6 +41,19 @@
<button id="isolateComponentBtn" class="chip">Isolate</button> <button id="isolateComponentBtn" class="chip">Isolate</button>
</div> </div>
<input id="instanceFilter" placeholder="Filter instances" aria-label="Filter instances" /> <input id="instanceFilter" placeholder="Filter instances" aria-label="Filter instances" />
<div class="quickCreate">
<input id="newComponentRefInput" placeholder="Ref (auto if empty)" aria-label="New component reference" />
<select id="newComponentTypeSelect" aria-label="New component type">
<option value="resistor">resistor</option>
<option value="capacitor">capacitor</option>
<option value="inductor">inductor</option>
<option value="diode">diode</option>
<option value="led">led</option>
<option value="connector">connector</option>
<option value="generic">generic</option>
</select>
<button id="addComponentBtn">Add Component</button>
</div>
<ul id="instanceList" class="list" aria-label="Instances list"></ul> <ul id="instanceList" class="list" aria-label="Instances list"></ul>
</section> </section>
<section> <section>
@ -49,6 +62,19 @@
<button id="isolateNetBtn" class="chip">Isolate</button> <button id="isolateNetBtn" class="chip">Isolate</button>
</div> </div>
<input id="netFilter" placeholder="Filter nets" aria-label="Filter nets" /> <input id="netFilter" placeholder="Filter nets" aria-label="Filter nets" />
<div class="quickCreate">
<input id="newQuickNetNameInput" placeholder="NET_1" aria-label="New net name" />
<select id="newQuickNetClassSelect" aria-label="New net class">
<option value="signal">signal</option>
<option value="analog">analog</option>
<option value="power">power</option>
<option value="ground">ground</option>
<option value="clock">clock</option>
<option value="bus">bus</option>
<option value="differential">differential</option>
</select>
<button id="addQuickNetBtn">Add Net</button>
</div>
<ul id="netList" class="list" aria-label="Nets list"></ul> <ul id="netList" class="list" aria-label="Nets list"></ul>
</section> </section>
<section class="legendSection" aria-label="Net color legend"> <section class="legendSection" aria-label="Net color legend">

View File

@ -272,6 +272,17 @@ textarea {
background: transparent; background: transparent;
} }
.quickCreate {
margin-top: 8px;
display: grid;
grid-template-columns: 1fr;
gap: 6px;
}
.quickCreate button {
justify-self: start;
}
.legendSection { .legendSection {
margin-top: 2px; margin-top: 2px;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 165 KiB

View File

@ -200,6 +200,16 @@ async function run() {
const mode = await page.locator("#renderModeSelect").inputValue(); const mode = await page.locator("#renderModeSelect").inputValue();
assert.equal(mode, "schematic_stub"); assert.equal(mode, "schematic_stub");
await page.locator("#newComponentTypeSelect").selectOption("resistor");
const beforeInstanceCount = await page.locator("[data-ref-item]").count();
await page.getByRole("button", { name: "Add Component" }).click();
await waitFor(async () => (await page.locator("[data-ref-item]").count()) >= beforeInstanceCount + 1);
await page.locator("#newQuickNetClassSelect").selectOption("signal");
await page.locator("#newQuickNetNameInput").fill("UI_TEST_NET");
await page.getByRole("button", { name: "Add Net" }).click();
await expectText(page, "#netList", /UI_TEST_NET/);
const dense = await readFile(DENSE_ANALOG_PATH, "utf8"); const dense = await readFile(DENSE_ANALOG_PATH, "utf8");
await page.locator("#jsonEditor").fill(dense); await page.locator("#jsonEditor").fill(dense);
await page.getByRole("button", { name: "Apply JSON" }).click(); await page.getByRole("button", { name: "Apply JSON" }).click();