Harden selection UX and add undo/redo history controls
This commit is contained in:
parent
6c5431a6e5
commit
85e5a345f1
163
frontend/app.js
163
frontend/app.js
@ -33,7 +33,11 @@ const state = {
|
|||||||
boxStartY: 0,
|
boxStartY: 0,
|
||||||
boxMoved: false,
|
boxMoved: false,
|
||||||
suppressCanvasClick: false,
|
suppressCanvasClick: false,
|
||||||
compileDebounceId: null
|
compileDebounceId: null,
|
||||||
|
historyPast: [],
|
||||||
|
historyFuture: [],
|
||||||
|
historyLimit: 80,
|
||||||
|
historyRestoring: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const el = {
|
const el = {
|
||||||
@ -109,6 +113,8 @@ const el = {
|
|||||||
copyReproBtn: document.getElementById("copyReproBtn"),
|
copyReproBtn: document.getElementById("copyReproBtn"),
|
||||||
autoLayoutBtn: document.getElementById("autoLayoutBtn"),
|
autoLayoutBtn: document.getElementById("autoLayoutBtn"),
|
||||||
autoTidyBtn: document.getElementById("autoTidyBtn"),
|
autoTidyBtn: document.getElementById("autoTidyBtn"),
|
||||||
|
undoBtn: document.getElementById("undoBtn"),
|
||||||
|
redoBtn: document.getElementById("redoBtn"),
|
||||||
renderModeSelect: document.getElementById("renderModeSelect"),
|
renderModeSelect: document.getElementById("renderModeSelect"),
|
||||||
isolateNetBtn: document.getElementById("isolateNetBtn"),
|
isolateNetBtn: document.getElementById("isolateNetBtn"),
|
||||||
isolateComponentBtn: document.getElementById("isolateComponentBtn"),
|
isolateComponentBtn: document.getElementById("isolateComponentBtn"),
|
||||||
@ -128,6 +134,76 @@ function clone(obj) {
|
|||||||
return JSON.parse(JSON.stringify(obj));
|
return JSON.parse(JSON.stringify(obj));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function captureHistorySnapshot() {
|
||||||
|
if (!state.model) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
model: clone(state.model),
|
||||||
|
selectedRefs: [...state.selectedRefs],
|
||||||
|
selectedNet: state.selectedNet,
|
||||||
|
selectedPin: state.selectedPin ? { ...state.selectedPin, nets: [...(state.selectedPin.nets ?? [])] } : null,
|
||||||
|
isolateNet: Boolean(state.isolateNet),
|
||||||
|
isolateComponent: Boolean(state.isolateComponent)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreHistorySnapshot(snapshot) {
|
||||||
|
if (!snapshot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.model = clone(snapshot.model);
|
||||||
|
setSelectedRefs(snapshot.selectedRefs ?? []);
|
||||||
|
state.selectedNet = snapshot.selectedNet ?? null;
|
||||||
|
state.selectedPin = snapshot.selectedPin ? { ...snapshot.selectedPin } : null;
|
||||||
|
state.isolateNet = Boolean(snapshot.isolateNet);
|
||||||
|
state.isolateComponent = Boolean(snapshot.isolateComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushHistory(reason = "edit") {
|
||||||
|
const snap = captureHistorySnapshot();
|
||||||
|
if (!snap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.historyPast.push({ reason, snapshot: snap });
|
||||||
|
if (state.historyPast.length > state.historyLimit) {
|
||||||
|
state.historyPast.splice(0, state.historyPast.length - state.historyLimit);
|
||||||
|
}
|
||||||
|
state.historyFuture = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performUndo() {
|
||||||
|
if (!state.historyPast.length || !state.model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const current = captureHistorySnapshot();
|
||||||
|
const prev = state.historyPast.pop();
|
||||||
|
if (!prev?.snapshot || !current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.historyFuture.push({ reason: prev.reason, snapshot: current });
|
||||||
|
state.historyRestoring = true;
|
||||||
|
restoreHistorySnapshot(prev.snapshot);
|
||||||
|
await compileModel(state.model, { keepView: true, source: "undo" });
|
||||||
|
state.historyRestoring = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performRedo() {
|
||||||
|
if (!state.historyFuture.length || !state.model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const current = captureHistorySnapshot();
|
||||||
|
const next = state.historyFuture.pop();
|
||||||
|
if (!next?.snapshot || !current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.historyPast.push({ reason: next.reason, snapshot: current });
|
||||||
|
state.historyRestoring = true;
|
||||||
|
restoreHistorySnapshot(next.snapshot);
|
||||||
|
await compileModel(state.model, { keepView: true, source: "redo" });
|
||||||
|
state.historyRestoring = false;
|
||||||
|
}
|
||||||
|
|
||||||
function hasSelectionModifier(evt) {
|
function hasSelectionModifier(evt) {
|
||||||
return Boolean(evt?.ctrlKey || evt?.metaKey || evt?.shiftKey);
|
return Boolean(evt?.ctrlKey || evt?.metaKey || evt?.shiftKey);
|
||||||
}
|
}
|
||||||
@ -945,12 +1021,14 @@ function focusIssue(issueId) {
|
|||||||
state.selectedNet = target.net;
|
state.selectedNet = target.net;
|
||||||
setSelectedRefs([]);
|
setSelectedRefs([]);
|
||||||
state.selectedPin = null;
|
state.selectedPin = null;
|
||||||
|
state.isolateComponent = false;
|
||||||
renderAll();
|
renderAll();
|
||||||
flashElements(`[data-net="${target.net}"], [data-net-label="${target.net}"], [data-net-junction="${target.net}"], [data-net-tie="${target.net}"]`);
|
flashElements(`[data-net="${target.net}"], [data-net-label="${target.net}"], [data-net-junction="${target.net}"], [data-net-tie="${target.net}"]`);
|
||||||
} else if (target.type === "component" && target.ref) {
|
} else if (target.type === "component" && target.ref) {
|
||||||
selectSingleRef(target.ref);
|
selectSingleRef(target.ref);
|
||||||
state.selectedNet = null;
|
state.selectedNet = null;
|
||||||
state.selectedPin = null;
|
state.selectedPin = null;
|
||||||
|
state.isolateNet = false;
|
||||||
renderAll();
|
renderAll();
|
||||||
flashElements(`[data-ref="${target.ref}"]`);
|
flashElements(`[data-ref="${target.ref}"]`);
|
||||||
} else if (target.type === "pin" && target.ref && target.pin) {
|
} else if (target.type === "pin" && target.ref && target.pin) {
|
||||||
@ -961,6 +1039,7 @@ function focusIssue(issueId) {
|
|||||||
pin: target.pin,
|
pin: target.pin,
|
||||||
nets: []
|
nets: []
|
||||||
};
|
};
|
||||||
|
state.isolateNet = false;
|
||||||
renderAll();
|
renderAll();
|
||||||
flashElements(`[data-pin-ref="${target.ref}"][data-pin-name="${target.pin}"]`);
|
flashElements(`[data-pin-ref="${target.ref}"][data-pin-name="${target.pin}"]`);
|
||||||
}
|
}
|
||||||
@ -990,6 +1069,15 @@ function bindSvgInteractions() {
|
|||||||
|
|
||||||
svg.querySelectorAll("[data-ref]").forEach((node) => {
|
svg.querySelectorAll("[data-ref]").forEach((node) => {
|
||||||
node.addEventListener("pointerdown", (evt) => {
|
node.addEventListener("pointerdown", (evt) => {
|
||||||
|
if (
|
||||||
|
evt.target.closest("[data-pin-ref]") ||
|
||||||
|
evt.target.closest("[data-net]") ||
|
||||||
|
evt.target.closest("[data-net-label]") ||
|
||||||
|
evt.target.closest("[data-net-junction]") ||
|
||||||
|
evt.target.closest("[data-net-tie]")
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const ref = node.getAttribute("data-ref");
|
const ref = node.getAttribute("data-ref");
|
||||||
@ -1001,6 +1089,7 @@ function bindSvgInteractions() {
|
|||||||
toggleSelectedRef(ref);
|
toggleSelectedRef(ref);
|
||||||
state.selectedNet = null;
|
state.selectedNet = null;
|
||||||
state.selectedPin = null;
|
state.selectedPin = null;
|
||||||
|
state.isolateNet = false;
|
||||||
renderAll();
|
renderAll();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1010,6 +1099,7 @@ function bindSvgInteractions() {
|
|||||||
}
|
}
|
||||||
state.selectedNet = null;
|
state.selectedNet = null;
|
||||||
state.selectedPin = null;
|
state.selectedPin = null;
|
||||||
|
state.isolateNet = false;
|
||||||
renderInstances();
|
renderInstances();
|
||||||
renderNets();
|
renderNets();
|
||||||
renderSelected();
|
renderSelected();
|
||||||
@ -1061,6 +1151,7 @@ function bindSvgInteractions() {
|
|||||||
state.selectedNet = net;
|
state.selectedNet = net;
|
||||||
setSelectedRefs([]);
|
setSelectedRefs([]);
|
||||||
state.selectedPin = null;
|
state.selectedPin = null;
|
||||||
|
state.isolateComponent = false;
|
||||||
renderAll();
|
renderAll();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -1090,6 +1181,7 @@ function bindSvgInteractions() {
|
|||||||
state.selectedPin = { ref, pin, nets };
|
state.selectedPin = { ref, pin, nets };
|
||||||
selectSingleRef(ref);
|
selectSingleRef(ref);
|
||||||
state.selectedNet = null;
|
state.selectedNet = null;
|
||||||
|
state.isolateNet = false;
|
||||||
renderAll();
|
renderAll();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -1117,6 +1209,8 @@ function renderAll() {
|
|||||||
|
|
||||||
el.isolateNetBtn.classList.toggle("activeChip", state.isolateNet);
|
el.isolateNetBtn.classList.toggle("activeChip", state.isolateNet);
|
||||||
el.isolateComponentBtn.classList.toggle("activeChip", state.isolateComponent);
|
el.isolateComponentBtn.classList.toggle("activeChip", state.isolateComponent);
|
||||||
|
el.undoBtn.disabled = state.historyPast.length === 0;
|
||||||
|
el.redoBtn.disabled = state.historyFuture.length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function compileModel(model, opts = {}) {
|
async function compileModel(model, opts = {}) {
|
||||||
@ -1550,6 +1644,7 @@ async function runLayoutAction(path) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pushHistory(path.includes("auto") ? "auto-layout" : "auto-tidy");
|
||||||
setStatus(path.includes("auto") ? "Auto layout..." : "Auto tidy...");
|
setStatus(path.includes("auto") ? "Auto layout..." : "Auto tidy...");
|
||||||
try {
|
try {
|
||||||
const out = await apiPost(path, {
|
const out = await apiPost(path, {
|
||||||
@ -1579,6 +1674,9 @@ async function loadSample() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const model = await res.json();
|
const model = await res.json();
|
||||||
|
if (state.model) {
|
||||||
|
pushHistory("load-sample");
|
||||||
|
}
|
||||||
setSelectedRefs([]);
|
setSelectedRefs([]);
|
||||||
state.selectedNet = null;
|
state.selectedNet = null;
|
||||||
state.selectedPin = null;
|
state.selectedPin = null;
|
||||||
@ -1602,6 +1700,7 @@ function setupEvents() {
|
|||||||
}
|
}
|
||||||
state.selectedNet = null;
|
state.selectedNet = null;
|
||||||
state.selectedPin = null;
|
state.selectedPin = null;
|
||||||
|
state.isolateNet = false;
|
||||||
renderAll();
|
renderAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1613,6 +1712,7 @@ function setupEvents() {
|
|||||||
state.selectedNet = item.getAttribute("data-net-item");
|
state.selectedNet = item.getAttribute("data-net-item");
|
||||||
setSelectedRefs([]);
|
setSelectedRefs([]);
|
||||||
state.selectedPin = null;
|
state.selectedPin = null;
|
||||||
|
state.isolateComponent = false;
|
||||||
renderAll();
|
renderAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1644,6 +1744,7 @@ function setupEvents() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pushHistory("component-edit");
|
||||||
const oldRef = inst.ref;
|
const oldRef = inst.ref;
|
||||||
updateInstance(oldRef, {
|
updateInstance(oldRef, {
|
||||||
ref: nextRef,
|
ref: nextRef,
|
||||||
@ -1687,6 +1788,7 @@ function setupEvents() {
|
|||||||
if (!inst) {
|
if (!inst) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
pushHistory("rotate");
|
||||||
const current = Number(inst.placement.rotation ?? 0);
|
const current = Number(inst.placement.rotation ?? 0);
|
||||||
inst.placement.rotation = ((Math.round(current / 90) * 90 + 90) % 360 + 360) % 360;
|
inst.placement.rotation = ((Math.round(current / 90) * 90 + 90) % 360 + 360) % 360;
|
||||||
inst.placement.locked = true;
|
inst.placement.locked = true;
|
||||||
@ -1700,6 +1802,7 @@ function setupEvents() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { ref, pin } = state.selectedPin;
|
const { ref, pin } = state.selectedPin;
|
||||||
|
pushHistory("pin-ui");
|
||||||
if (!setPinUi(ref, pin, { show_net_label: el.showPinNetLabelInput.checked })) {
|
if (!setPinUi(ref, pin, { show_net_label: el.showPinNetLabelInput.checked })) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1742,6 +1845,7 @@ function setupEvents() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pushHistory("pin-props");
|
||||||
const beforeName = sym.pins[idx].name;
|
const beforeName = sym.pins[idx].name;
|
||||||
sym.pins[idx] = {
|
sym.pins[idx] = {
|
||||||
...sym.pins[idx],
|
...sym.pins[idx],
|
||||||
@ -1766,6 +1870,7 @@ function setupEvents() {
|
|||||||
el.jsonFeedback.textContent = "Choose a net first.";
|
el.jsonFeedback.textContent = "Choose a net first.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
pushHistory("connect-pin");
|
||||||
const out = connectPinToNet(state.selectedPin.ref, state.selectedPin.pin, netName);
|
const out = connectPinToNet(state.selectedPin.ref, state.selectedPin.pin, netName);
|
||||||
if (!out.ok) {
|
if (!out.ok) {
|
||||||
el.jsonFeedback.textContent = out.message;
|
el.jsonFeedback.textContent = out.message;
|
||||||
@ -1782,6 +1887,7 @@ function setupEvents() {
|
|||||||
}
|
}
|
||||||
const name = normalizeNetName(el.newNetNameInput.value || el.newNetNameInput.placeholder || nextAutoNetName());
|
const name = normalizeNetName(el.newNetNameInput.value || el.newNetNameInput.placeholder || nextAutoNetName());
|
||||||
const cls = el.newNetClassInput.value;
|
const cls = el.newNetClassInput.value;
|
||||||
|
pushHistory("create-net");
|
||||||
const out = connectPinToNet(state.selectedPin.ref, state.selectedPin.pin, name, { netClass: cls });
|
const out = connectPinToNet(state.selectedPin.ref, state.selectedPin.pin, name, { netClass: cls });
|
||||||
if (!out.ok) {
|
if (!out.ok) {
|
||||||
el.jsonFeedback.textContent = out.message;
|
el.jsonFeedback.textContent = out.message;
|
||||||
@ -1799,6 +1905,7 @@ function setupEvents() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const netName = btn.getAttribute("data-disconnect-net");
|
const netName = btn.getAttribute("data-disconnect-net");
|
||||||
|
pushHistory("disconnect-pin");
|
||||||
const out = disconnectPinFromNet(state.selectedPin.ref, state.selectedPin.pin, netName);
|
const out = disconnectPinFromNet(state.selectedPin.ref, state.selectedPin.pin, netName);
|
||||||
if (!out.ok) {
|
if (!out.ok) {
|
||||||
el.jsonFeedback.textContent = out.message;
|
el.jsonFeedback.textContent = out.message;
|
||||||
@ -1812,6 +1919,7 @@ function setupEvents() {
|
|||||||
if (!state.selectedNet) {
|
if (!state.selectedNet) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
pushHistory("net-edit");
|
||||||
const oldName = state.selectedNet;
|
const oldName = state.selectedNet;
|
||||||
const renamed = renameNet(oldName, el.netNameInput.value);
|
const renamed = renameNet(oldName, el.netNameInput.value);
|
||||||
if (!renamed.ok) {
|
if (!renamed.ok) {
|
||||||
@ -1838,6 +1946,7 @@ function setupEvents() {
|
|||||||
el.jsonFeedback.textContent = "Provide both ref and pin.";
|
el.jsonFeedback.textContent = "Provide both ref and pin.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
pushHistory("net-node-add");
|
||||||
const out = connectPinToNet(ref, pin, state.selectedNet);
|
const out = connectPinToNet(ref, pin, state.selectedNet);
|
||||||
if (!out.ok) {
|
if (!out.ok) {
|
||||||
el.jsonFeedback.textContent = out.message;
|
el.jsonFeedback.textContent = out.message;
|
||||||
@ -1856,6 +1965,7 @@ function setupEvents() {
|
|||||||
const netName = state.selectedNet;
|
const netName = state.selectedNet;
|
||||||
const [ref, ...pinParts] = String(btn.getAttribute("data-remove-node")).split(".");
|
const [ref, ...pinParts] = String(btn.getAttribute("data-remove-node")).split(".");
|
||||||
const pin = pinParts.join(".");
|
const pin = pinParts.join(".");
|
||||||
|
pushHistory("net-node-remove");
|
||||||
const out = disconnectPinFromNet(ref, pin, netName);
|
const out = disconnectPinFromNet(ref, pin, netName);
|
||||||
if (!out.ok) {
|
if (!out.ok) {
|
||||||
el.jsonFeedback.textContent = out.message;
|
el.jsonFeedback.textContent = out.message;
|
||||||
@ -1933,6 +2043,7 @@ function setupEvents() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pushHistory("symbol-edit");
|
||||||
for (const entry of parsedPins) {
|
for (const entry of parsedPins) {
|
||||||
if (entry.oldName && entry.oldName !== entry.pin.name) {
|
if (entry.oldName && entry.oldName !== entry.pin.name) {
|
||||||
renamePinAcrossSymbolInstances(inst.symbol, entry.oldName, entry.pin.name);
|
renamePinAcrossSymbolInstances(inst.symbol, entry.oldName, entry.pin.name);
|
||||||
@ -2154,6 +2265,7 @@ function setupEvents() {
|
|||||||
state.dragMoved = false;
|
state.dragMoved = false;
|
||||||
|
|
||||||
if (wasDragging && moved && state.model) {
|
if (wasDragging && moved && state.model) {
|
||||||
|
pushHistory("drag-move");
|
||||||
for (const [ref, pos] of Object.entries(dragSnapshot ?? {})) {
|
for (const [ref, pos] of Object.entries(dragSnapshot ?? {})) {
|
||||||
const inst = state.model.instances.find((x) => x.ref === ref);
|
const inst = state.model.instances.find((x) => x.ref === ref);
|
||||||
if (inst) {
|
if (inst) {
|
||||||
@ -2178,12 +2290,29 @@ function setupEvents() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("keydown", (evt) => {
|
window.addEventListener("keydown", (evt) => {
|
||||||
|
const mod = evt.ctrlKey || evt.metaKey;
|
||||||
|
if (mod && evt.key.toLowerCase() === "z") {
|
||||||
|
evt.preventDefault();
|
||||||
|
if (evt.shiftKey) {
|
||||||
|
void performRedo();
|
||||||
|
} else {
|
||||||
|
void performUndo();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mod && evt.key.toLowerCase() === "y") {
|
||||||
|
evt.preventDefault();
|
||||||
|
void performRedo();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (evt.code === "Space") {
|
if (evt.code === "Space") {
|
||||||
if (isTypingContext(evt.target)) {
|
if (isTypingContext(evt.target)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.selectedRefs.length && state.model && !evt.repeat) {
|
if (state.selectedRefs.length && state.model && !evt.repeat) {
|
||||||
|
pushHistory("rotate-hotkey");
|
||||||
for (const ref of state.selectedRefs) {
|
for (const ref of state.selectedRefs) {
|
||||||
const inst = state.model.instances.find((x) => x.ref === ref);
|
const inst = state.model.instances.find((x) => x.ref === ref);
|
||||||
if (!inst) {
|
if (!inst) {
|
||||||
@ -2212,7 +2341,20 @@ function setupEvents() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (evt.code === "Escape") {
|
if (evt.code === "Escape") {
|
||||||
closeSchemaModal();
|
if (!el.schemaModal.classList.contains("hidden")) {
|
||||||
|
closeSchemaModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hadSelection = Boolean(state.selectedRefs.length || state.selectedNet || state.selectedPin);
|
||||||
|
const hadIsolation = Boolean(state.isolateNet || state.isolateComponent);
|
||||||
|
if (hadSelection || hadIsolation) {
|
||||||
|
setSelectedRefs([]);
|
||||||
|
state.selectedNet = null;
|
||||||
|
state.selectedPin = null;
|
||||||
|
state.isolateNet = false;
|
||||||
|
state.isolateComponent = false;
|
||||||
|
renderAll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -2290,6 +2432,9 @@ function setupEvents() {
|
|||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(el.jsonEditor.value);
|
const parsed = JSON.parse(el.jsonEditor.value);
|
||||||
const before = state.model ? clone(state.model) : null;
|
const before = state.model ? clone(state.model) : null;
|
||||||
|
if (state.model) {
|
||||||
|
pushHistory("apply-json");
|
||||||
|
}
|
||||||
el.jsonFeedback.textContent = "Applying JSON...";
|
el.jsonFeedback.textContent = "Applying JSON...";
|
||||||
setSelectedRefs([]);
|
setSelectedRefs([]);
|
||||||
state.selectedNet = null;
|
state.selectedNet = null;
|
||||||
@ -2303,6 +2448,9 @@ function setupEvents() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
el.newProjectBtn.addEventListener("click", async () => {
|
el.newProjectBtn.addEventListener("click", async () => {
|
||||||
|
if (state.model) {
|
||||||
|
pushHistory("new-project");
|
||||||
|
}
|
||||||
setSelectedRefs([]);
|
setSelectedRefs([]);
|
||||||
state.selectedNet = null;
|
state.selectedNet = null;
|
||||||
state.selectedPin = null;
|
state.selectedPin = null;
|
||||||
@ -2319,6 +2467,14 @@ function setupEvents() {
|
|||||||
await runLayoutAction("/layout/tidy");
|
await runLayoutAction("/layout/tidy");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
el.undoBtn.addEventListener("click", async () => {
|
||||||
|
await performUndo();
|
||||||
|
});
|
||||||
|
|
||||||
|
el.redoBtn.addEventListener("click", async () => {
|
||||||
|
await performRedo();
|
||||||
|
});
|
||||||
|
|
||||||
el.importBtn.addEventListener("click", () => {
|
el.importBtn.addEventListener("click", () => {
|
||||||
el.fileInput.click();
|
el.fileInput.click();
|
||||||
});
|
});
|
||||||
@ -2332,6 +2488,9 @@ function setupEvents() {
|
|||||||
try {
|
try {
|
||||||
const content = await file.text();
|
const content = await file.text();
|
||||||
const parsed = JSON.parse(content);
|
const parsed = JSON.parse(content);
|
||||||
|
if (state.model) {
|
||||||
|
pushHistory("import-json");
|
||||||
|
}
|
||||||
setSelectedRefs([]);
|
setSelectedRefs([]);
|
||||||
state.selectedNet = null;
|
state.selectedNet = null;
|
||||||
state.selectedPin = null;
|
state.selectedPin = null;
|
||||||
|
|||||||
@ -19,6 +19,8 @@
|
|||||||
<button id="exportBtn">Export JSON</button>
|
<button id="exportBtn">Export JSON</button>
|
||||||
<button id="autoLayoutBtn">Auto Layout</button>
|
<button id="autoLayoutBtn">Auto Layout</button>
|
||||||
<button id="autoTidyBtn">Auto Tidy</button>
|
<button id="autoTidyBtn">Auto Tidy</button>
|
||||||
|
<button id="undoBtn" title="Undo (Ctrl/Cmd+Z)">Undo</button>
|
||||||
|
<button id="redoBtn" title="Redo (Ctrl/Cmd+Shift+Z)">Redo</button>
|
||||||
<label class="inlineSelect">
|
<label class="inlineSelect">
|
||||||
Mode
|
Mode
|
||||||
<select id="renderModeSelect">
|
<select id="renderModeSelect">
|
||||||
|
|||||||
@ -69,6 +69,11 @@ button {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
button.primary {
|
button.primary {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user