Add UI-native component/net creation workflows and QA coverage
@ -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;
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
|
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();
|
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();
|
||||||
|
|||||||