Fix undo gaps and add right-click menu items 16-20
Undo fixes: - Move undo now redraws deviceLayer so devices visually snap back - Label, reference, gauge, color, and all wire/device property changes are now undoable - Duplicate and paste are now undoable (deletes cloned devices on Ctrl+Z) Right-click menu additions (roadmap 16-20): - Align & distribute: left/right/top/bottom edges, H/V spacing for multi-select - Bring to front / Send to back via zOrder in device.properties - Copy/paste wire style (color, gauge, stripe, twisted pair, shielded) - Select all wires on net highlights every wire sharing the same label - Lock device: prevents moves, shows padlock badge, persists across refresh Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+76
-4
@@ -32,6 +32,7 @@ class DiagramCanvas {
|
||||
this._multiDragWireWaypoints = null; // wireId -> [{x,y}] snapshot at dragstart
|
||||
this._bundleData = []; // array of bundle objects
|
||||
this._bundleNodes = []; // Konva shapes for bundle overlays (on groupLayer)
|
||||
this._netHighlightIds = new Set(); // wire IDs currently net-highlighted
|
||||
|
||||
this._initStage(containerId);
|
||||
this._initZoomPan();
|
||||
@@ -505,7 +506,10 @@ class DiagramCanvas {
|
||||
setMode(mode) {
|
||||
this.mode = mode;
|
||||
this.stage.container().classList.toggle("wire-mode", mode === "wire");
|
||||
this.deviceNodes.forEach(g => g.draggable(mode === "select"));
|
||||
this.deviceNodes.forEach((g, id) => {
|
||||
const d = this.deviceData.get(id);
|
||||
g.draggable(mode === "select" && !d?.properties?.locked);
|
||||
});
|
||||
if (mode !== "wire") this._cancelWire();
|
||||
}
|
||||
|
||||
@@ -532,9 +536,12 @@ class DiagramCanvas {
|
||||
this._multiDragAnchorId = null; this._multiDragStartPos = null;
|
||||
this._bundleNodes = []; this._bundleData = [];
|
||||
|
||||
const sorted = [...(diagram.devices || [])].sort((a, b) =>
|
||||
(a.device_type === "group" ? -1 : 0) - (b.device_type === "group" ? -1 : 0)
|
||||
);
|
||||
const sorted = [...(diagram.devices || [])].sort((a, b) => {
|
||||
const aGroup = a.device_type === "group" ? 0 : 1;
|
||||
const bGroup = b.device_type === "group" ? 0 : 1;
|
||||
if (aGroup !== bGroup) return aGroup - bGroup;
|
||||
return (a.properties?.zOrder || 0) - (b.properties?.zOrder || 0);
|
||||
});
|
||||
sorted.forEach(d => {
|
||||
this.deviceData.set(d.id, d);
|
||||
this._renderDevice(d);
|
||||
@@ -630,6 +637,8 @@ class DiagramCanvas {
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this._netHighlightIds.forEach(id => this._wireHighlight(id, false));
|
||||
this._netHighlightIds.clear();
|
||||
this.selectedDeviceIds.forEach(id => this._highlightDevice(id, false));
|
||||
this.selectedDeviceIds.clear();
|
||||
this._clearResizeHandles();
|
||||
@@ -1163,6 +1172,14 @@ class DiagramCanvas {
|
||||
|
||||
(device.pins || []).forEach(pin => this._addPin(group, device, pin));
|
||||
|
||||
if (device.properties?.locked) {
|
||||
group.add(new Konva.Text({
|
||||
name: "lock-badge", text: "🔒",
|
||||
x: device.width - 16, y: 3, fontSize: 11, listening: false,
|
||||
}));
|
||||
group.draggable(false);
|
||||
}
|
||||
|
||||
group.on("contextmenu", (e) => {
|
||||
e.evt.preventDefault();
|
||||
e.cancelBubble = true;
|
||||
@@ -1213,6 +1230,7 @@ class DiagramCanvas {
|
||||
});
|
||||
group.on("dragstart", () => {
|
||||
if (this.wireStart) { group.stopDrag(); return; }
|
||||
if (device.properties?.locked) { group.stopDrag(); return; }
|
||||
if (device.device_type !== "group") group.moveToTop();
|
||||
this._clearResizeHandles(); // hide during drag
|
||||
if (this.selectedDeviceIds.size > 1 && this.selectedDeviceIds.has(device.id)) {
|
||||
@@ -1807,4 +1825,58 @@ class DiagramCanvas {
|
||||
this._handleNodes = [];
|
||||
this.uiLayer.batchDraw();
|
||||
}
|
||||
|
||||
// ── Layer order ───────────────────────────────────────────────────────────────
|
||||
|
||||
bringToFront(id) {
|
||||
const g = this.deviceNodes.get(id);
|
||||
const d = this.deviceData.get(id);
|
||||
if (!g || !d) return;
|
||||
g.moveToTop();
|
||||
(d.device_type === "group" ? this.groupLayer : this.deviceLayer).batchDraw();
|
||||
}
|
||||
|
||||
sendToBack(id) {
|
||||
const g = this.deviceNodes.get(id);
|
||||
const d = this.deviceData.get(id);
|
||||
if (!g || !d) return;
|
||||
g.moveToBottom();
|
||||
(d.device_type === "group" ? this.groupLayer : this.deviceLayer).batchDraw();
|
||||
}
|
||||
|
||||
// ── Lock / unlock ─────────────────────────────────────────────────────────────
|
||||
|
||||
setDeviceLocked(id, locked) {
|
||||
const d = this.deviceData.get(id);
|
||||
const g = this.deviceNodes.get(id);
|
||||
if (!d || !g) return;
|
||||
d.properties = { ...(d.properties || {}), locked };
|
||||
g.draggable(!locked && this.mode === "select");
|
||||
const existing = g.findOne(".lock-badge");
|
||||
if (existing) existing.destroy();
|
||||
if (locked) {
|
||||
g.add(new Konva.Text({
|
||||
name: "lock-badge", text: "🔒",
|
||||
x: d.width - 16, y: 3, fontSize: 11, listening: false,
|
||||
}));
|
||||
}
|
||||
(d.device_type === "group" ? this.groupLayer : this.deviceLayer).batchDraw();
|
||||
}
|
||||
|
||||
// ── Net highlight ─────────────────────────────────────────────────────────────
|
||||
|
||||
highlightNet(label) {
|
||||
this._netHighlightIds.forEach(id => {
|
||||
if (id !== this.selectedId) this._wireHighlight(id, false);
|
||||
});
|
||||
this._netHighlightIds.clear();
|
||||
if (!label) return;
|
||||
this.wireData.forEach((wire, id) => {
|
||||
if (wire.label === label) {
|
||||
this._netHighlightIds.add(id);
|
||||
this._wireHighlight(id, true);
|
||||
}
|
||||
});
|
||||
this.wireLayer.batchDraw();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user