Add UI-native component/net creation workflows and QA coverage
@ -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;
|
||||
|
||||
@ -41,6 +41,19 @@
|
||||
<button id="isolateComponentBtn" class="chip">Isolate</button>
|
||||
</div>
|
||||
<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>
|
||||
</section>
|
||||
<section>
|
||||
@ -49,6 +62,19 @@
|
||||
<button id="isolateNetBtn" class="chip">Isolate</button>
|
||||
</div>
|
||||
<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>
|
||||
</section>
|
||||
<section class="legendSection" aria-label="Net color legend">
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 177 KiB After Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 255 KiB After Width: | Height: | Size: 269 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 165 KiB |
@ -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();
|
||||
|
||||