diff --git a/frontend/app.js b/frontend/app.js index e850b2d..df9e3c6 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -53,6 +53,12 @@ const el = { netList: document.getElementById("netList"), instanceFilter: document.getElementById("instanceFilter"), 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"), canvasInner: document.getElementById("canvasInner"), selectionBox: document.getElementById("selectionBox"), @@ -295,6 +301,17 @@ function nextRefLike(baseRef) { 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) { return String(text ?? "") .replaceAll("&", "&") @@ -2257,6 +2274,65 @@ function setupEvents() { }); el.instanceList.addEventListener("scroll", renderInstances, { 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) => { if (!section) { return; diff --git a/frontend/index.html b/frontend/index.html index f3526fe..5f28258 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -41,6 +41,19 @@ +
+ + + +
@@ -49,6 +62,19 @@ +
+ + + +
diff --git a/frontend/styles.css b/frontend/styles.css index 6f5a112..09f008a 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -272,6 +272,17 @@ textarea { background: transparent; } +.quickCreate { + margin-top: 8px; + display: grid; + grid-template-columns: 1fr; + gap: 6px; +} + +.quickCreate button { + justify-self: start; +} + .legendSection { margin-top: 2px; } diff --git a/tests/baselines/ui/dense-analog.png b/tests/baselines/ui/dense-analog.png index e7bdb73..9183214 100644 Binary files a/tests/baselines/ui/dense-analog.png and b/tests/baselines/ui/dense-analog.png differ diff --git a/tests/baselines/ui/explicit-mode-auto-tidy.png b/tests/baselines/ui/explicit-mode-auto-tidy.png index 82c864b..fc30f58 100644 Binary files a/tests/baselines/ui/explicit-mode-auto-tidy.png and b/tests/baselines/ui/explicit-mode-auto-tidy.png differ diff --git a/tests/baselines/ui/initial.png b/tests/baselines/ui/initial.png index 1405139..ab1722a 100644 Binary files a/tests/baselines/ui/initial.png and b/tests/baselines/ui/initial.png differ diff --git a/tests/baselines/ui/laptop-viewport.png b/tests/baselines/ui/laptop-viewport.png index 899f554..d610b0f 100644 Binary files a/tests/baselines/ui/laptop-viewport.png and b/tests/baselines/ui/laptop-viewport.png differ diff --git a/tests/baselines/ui/post-migration-apply.png b/tests/baselines/ui/post-migration-apply.png index 917701e..ca4d544 100644 Binary files a/tests/baselines/ui/post-migration-apply.png and b/tests/baselines/ui/post-migration-apply.png differ diff --git a/tests/baselines/ui/selected-u2.png b/tests/baselines/ui/selected-u2.png index cafef3e..90b295a 100644 Binary files a/tests/baselines/ui/selected-u2.png and b/tests/baselines/ui/selected-u2.png differ diff --git a/tests/ui-regression-runner.js b/tests/ui-regression-runner.js index e97cbcd..83107cd 100644 --- a/tests/ui-regression-runner.js +++ b/tests/ui-regression-runner.js @@ -200,6 +200,16 @@ async function run() { const mode = await page.locator("#renderModeSelect").inputValue(); 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"); await page.locator("#jsonEditor").fill(dense); await page.getByRole("button", { name: "Apply JSON" }).click();