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:
+5
-5
@@ -10,11 +10,11 @@
|
||||
- [x] 15. **Wire labels on canvas** — wire label + length rendered mid-wire as a small tag
|
||||
|
||||
### Right-Click Menu Additions
|
||||
- [ ] 16. **Align & distribute** — right-click multi-select → Align Left / Align Right / Align Top / Align Bottom / Distribute Horizontally / Distribute Vertically
|
||||
- [ ] 17. **Send to back / Bring to front** — right-click any device → layer order shortcuts (especially useful for Section Boxes)
|
||||
- [ ] 18. **Copy wire properties** — right-click a wire → "Copy style"; then right-click another wire → "Paste style" to apply color/gauge/stripe
|
||||
- [ ] 19. **Select all wires on net** — right-click a wire → "Select all on net" highlights every wire sharing the same wire label/net name
|
||||
- [ ] 20. **Lock device** — right-click → "Lock position" prevents accidental moves; locked devices show a padlock badge
|
||||
- [x] 16. **Align & distribute** — right-click multi-select → Align Left / Align Right / Align Top / Align Bottom / Distribute Horizontally / Distribute Vertically
|
||||
- [x] 17. **Send to back / Bring to front** — right-click any device → layer order shortcuts (especially useful for Section Boxes)
|
||||
- [x] 18. **Copy wire properties** — right-click a wire → "Copy style"; then right-click another wire → "Paste style" to apply color/gauge/stripe
|
||||
- [x] 19. **Select all wires on net** — right-click a wire → "Select all on net" highlights every wire sharing the same wire label/net name
|
||||
- [x] 20. **Lock device** — right-click → "Lock position" prevents accidental moves; locked devices show a padlock badge
|
||||
|
||||
### Productivity
|
||||
- [ ] 21. **Find / search** — toolbar search box filters devices by reference/label and highlights matches on canvas; Ctrl+F shortcut
|
||||
|
||||
+171
-21
@@ -11,6 +11,7 @@ class WiringApp {
|
||||
this._octopartReady = null; // null=unknown, true/false after status check
|
||||
this._bundles = []; // wire bundles for current diagram
|
||||
this._updatingBundleDropdown = false;
|
||||
this._copiedWireStyle = null; // clipboard for wire style copy/paste
|
||||
|
||||
this.canvas = new DiagramCanvas("canvas-container", {
|
||||
onDeviceSelected: (d) => this._showDeviceProps(d),
|
||||
@@ -83,6 +84,67 @@ class WiringApp {
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// ── Align & Distribute ────────────────────────────────────────────────────────
|
||||
|
||||
async _alignDevices(edge) {
|
||||
const ids = [...this.canvas.selectedIds];
|
||||
if (ids.length < 2) return;
|
||||
const boxes = ids.map(id => {
|
||||
const d = this.canvas.deviceData.get(id);
|
||||
return { id, x: d.x, y: d.y, w: d.width || 80, h: d.height || 60 };
|
||||
});
|
||||
let target;
|
||||
if (edge === "left") target = Math.min(...boxes.map(b => b.x));
|
||||
if (edge === "right") target = Math.max(...boxes.map(b => b.x + b.w));
|
||||
if (edge === "top") target = Math.min(...boxes.map(b => b.y));
|
||||
if (edge === "bottom") target = Math.max(...boxes.map(b => b.y + b.h));
|
||||
for (const b of boxes) {
|
||||
const d = this.canvas.deviceData.get(b.id);
|
||||
if (edge === "left") d.x = target;
|
||||
if (edge === "right") d.x = target - b.w;
|
||||
if (edge === "top") d.y = target;
|
||||
if (edge === "bottom") d.y = target - b.h;
|
||||
this.canvas.updateDevice(d);
|
||||
await api.devices.update(b.id, { x: d.x, y: d.y }).catch(console.error);
|
||||
}
|
||||
this._flashSaved();
|
||||
}
|
||||
|
||||
async _distributeDevices(axis) {
|
||||
const ids = [...this.canvas.selectedIds];
|
||||
if (ids.length < 3) return;
|
||||
const boxes = ids.map(id => {
|
||||
const d = this.canvas.deviceData.get(id);
|
||||
return { id, x: d.x, y: d.y, w: d.width || 80, h: d.height || 60 };
|
||||
});
|
||||
if (axis === "h") {
|
||||
boxes.sort((a, b) => a.x - b.x);
|
||||
const totalW = boxes.reduce((s, b) => s + b.w, 0);
|
||||
const gap = (boxes[boxes.length - 1].x + boxes[boxes.length - 1].w - boxes[0].x - totalW) / (boxes.length - 1);
|
||||
let cursor = boxes[0].x + boxes[0].w;
|
||||
for (let i = 1; i < boxes.length - 1; i++) {
|
||||
const d = this.canvas.deviceData.get(boxes[i].id);
|
||||
d.x = Math.round(cursor + gap);
|
||||
this.canvas.updateDevice(d);
|
||||
await api.devices.update(boxes[i].id, { x: d.x }).catch(console.error);
|
||||
cursor = d.x + boxes[i].w;
|
||||
}
|
||||
} else {
|
||||
boxes.sort((a, b) => a.y - b.y);
|
||||
const totalH = boxes.reduce((s, b) => s + b.h, 0);
|
||||
const gap = (boxes[boxes.length - 1].y + boxes[boxes.length - 1].h - boxes[0].y - totalH) / (boxes.length - 1);
|
||||
let cursor = boxes[0].y + boxes[0].h;
|
||||
for (let i = 1; i < boxes.length - 1; i++) {
|
||||
const d = this.canvas.deviceData.get(boxes[i].id);
|
||||
d.y = Math.round(cursor + gap);
|
||||
this.canvas.updateDevice(d);
|
||||
await api.devices.update(boxes[i].id, { y: d.y }).catch(console.error);
|
||||
cursor = d.y + boxes[i].h;
|
||||
}
|
||||
}
|
||||
this._flashSaved();
|
||||
}
|
||||
|
||||
// ── Undo ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
_pushUndo(fn) {
|
||||
@@ -99,7 +161,6 @@ class WiringApp {
|
||||
if (!this._preDrag) return;
|
||||
const { positions, waypoints } = this._preDrag;
|
||||
this._preDrag = null;
|
||||
// Snapshot post-drag state for redo (not needed — single-level undo)
|
||||
this._pushUndo(async () => {
|
||||
for (const [id, { x, y }] of positions) {
|
||||
const g = this.canvas.deviceNodes.get(id);
|
||||
@@ -116,6 +177,7 @@ class WiringApp {
|
||||
await api.wires.update(wId, { waypoints: wire.waypoints }).catch(console.error);
|
||||
}
|
||||
}
|
||||
this.canvas.deviceLayer.batchDraw();
|
||||
this.canvas.wireLayer.batchDraw();
|
||||
this._flashSaved();
|
||||
});
|
||||
@@ -431,34 +493,61 @@ class WiringApp {
|
||||
|
||||
_showCtxDevice(id, x, y) {
|
||||
const d = this.canvas.deviceData.get(id);
|
||||
const isMulti = this.canvas.selectedDeviceIds.size > 1;
|
||||
const count = this.canvas.selectedDeviceIds.size;
|
||||
const isMulti = count > 1;
|
||||
const isGroup = d?.device_type === "group";
|
||||
const isLocked = !!d?.properties?.locked;
|
||||
|
||||
const items = isMulti ? [
|
||||
{ action: "dup", icon: "⊕", label: `Duplicate ${this.canvas.selectedDeviceIds.size} items` },
|
||||
{ action: "zoom", icon: "⊞", label: "Zoom to selection" },
|
||||
{ action: "dup", icon: "⊕", label: `Duplicate ${count} items` },
|
||||
{ action: "zoom", icon: "⊞", label: "Zoom to selection" },
|
||||
"---",
|
||||
{ action: "del", icon: "✕", label: `Delete ${this.canvas.selectedDeviceIds.size} items`, danger: true },
|
||||
] : isGroup ? [
|
||||
{ action: "dup", icon: "⊕", label: "Duplicate" },
|
||||
{ action: "align-left", icon: "⇤", label: "Align Left" },
|
||||
{ action: "align-right", icon: "⇥", label: "Align Right" },
|
||||
{ action: "align-top", icon: "⇡", label: "Align Top" },
|
||||
{ action: "align-bottom", icon: "⇣", label: "Align Bottom" },
|
||||
...(count >= 3 ? [
|
||||
"---",
|
||||
{ action: "dist-h", icon: "↔", label: "Distribute Horizontally" },
|
||||
{ action: "dist-v", icon: "↕", label: "Distribute Vertically" },
|
||||
] : []),
|
||||
"---",
|
||||
{ action: "del", icon: "✕", label: "Delete", danger: true },
|
||||
{ action: "del", icon: "✕", label: `Delete ${count} items`, danger: true },
|
||||
] : [
|
||||
{ action: "dup", icon: "⊕", label: "Duplicate" },
|
||||
{ action: "addpin", icon: "+", label: "Add Pin" },
|
||||
{ action: "savelb", icon: "⬆", label: "Save to Library" },
|
||||
{ action: "dup", icon: "⊕", label: "Duplicate" },
|
||||
...(!isGroup ? [{ action: "addpin", icon: "+", label: "Add Pin" }] : []),
|
||||
...(!isGroup ? [{ action: "savelb", icon: "⬆", label: "Save to Library" }] : []),
|
||||
"---",
|
||||
{ action: "del", icon: "✕", label: "Delete", danger: true },
|
||||
{ action: "front", icon: "▲", label: "Bring to Front" },
|
||||
{ action: "back", icon: "▼", label: "Send to Back" },
|
||||
"---",
|
||||
isLocked
|
||||
? { action: "unlock", icon: "🔓", label: "Unlock Position" }
|
||||
: { action: "lock", icon: "🔒", label: "Lock Position" },
|
||||
"---",
|
||||
{ action: "del", icon: "✕", label: "Delete", danger: true },
|
||||
];
|
||||
|
||||
this._openCtx(x, y, items);
|
||||
this._ctxMenu().onclick = (e) => {
|
||||
this._ctxMenu().onclick = async (e) => {
|
||||
const action = e.target.closest(".ctx-item")?.dataset.action;
|
||||
this._closeCtx();
|
||||
if (!action) return;
|
||||
if (action === "dup") this.duplicateSelected();
|
||||
if (action === "del") this.deleteSelected();
|
||||
if (action === "zoom") this.canvas.zoomToSelection();
|
||||
if (action === "addpin" && d) this._addDevicePin(d);
|
||||
if (action === "savelb" && d) this._saveDeviceAsConnector(d);
|
||||
if (action === "dup") this.duplicateSelected();
|
||||
if (action === "del") this.deleteSelected();
|
||||
if (action === "zoom") this.canvas.zoomToSelection();
|
||||
if (action === "addpin" && d) this._addDevicePin(d);
|
||||
if (action === "savelb" && d) this._saveDeviceAsConnector(d);
|
||||
if (action === "front") { this.canvas.bringToFront(id); const dd = this.canvas.deviceData.get(id); if (dd) { dd.properties = { ...(dd.properties||{}), zOrder: Date.now() }; api.devices.update(id, { properties: dd.properties }).catch(console.error); } }
|
||||
if (action === "back") { this.canvas.sendToBack(id); const dd = this.canvas.deviceData.get(id); if (dd) { dd.properties = { ...(dd.properties||{}), zOrder: -Date.now() }; api.devices.update(id, { properties: dd.properties }).catch(console.error); } }
|
||||
if (action === "lock") { this.canvas.setDeviceLocked(id, true); const dd = this.canvas.deviceData.get(id); if (dd) await api.devices.update(id, { properties: dd.properties }).catch(console.error); }
|
||||
if (action === "unlock") { this.canvas.setDeviceLocked(id, false); const dd = this.canvas.deviceData.get(id); if (dd) await api.devices.update(id, { properties: dd.properties }).catch(console.error); }
|
||||
if (action === "align-left") this._alignDevices("left");
|
||||
if (action === "align-right") this._alignDevices("right");
|
||||
if (action === "align-top") this._alignDevices("top");
|
||||
if (action === "align-bottom") this._alignDevices("bottom");
|
||||
if (action === "dist-h") this._distributeDevices("h");
|
||||
if (action === "dist-v") this._distributeDevices("v");
|
||||
};
|
||||
}
|
||||
|
||||
@@ -466,13 +555,17 @@ class WiringApp {
|
||||
const wire = this.canvas.wireData.get(id);
|
||||
const hasWaypoints = (wire?.waypoints?.length ?? 0) > 0;
|
||||
const items = [
|
||||
{ action: "copystyle", icon: "⎘", label: "Copy Style" },
|
||||
...(this._copiedWireStyle ? [{ action: "pastestyle", icon: "⎗", label: "Paste Style" }] : []),
|
||||
...(wire?.label ? [{ action: "selectnet", icon: "⋈", label: `Select net "${wire.label}"` }] : []),
|
||||
"---",
|
||||
{ action: "straighten", icon: "⤢", label: "Straighten wire" },
|
||||
...(hasWaypoints ? [{ action: "clearwp", icon: "⌀", label: "Clear waypoints" }] : []),
|
||||
"---",
|
||||
{ action: "del", icon: "✕", label: "Delete wire", danger: true },
|
||||
];
|
||||
this._openCtx(x, y, items);
|
||||
this._ctxMenu().onclick = (e) => {
|
||||
this._ctxMenu().onclick = async (e) => {
|
||||
const action = e.target.closest(".ctx-item")?.dataset.action;
|
||||
this._closeCtx();
|
||||
if (!action) return;
|
||||
@@ -483,6 +576,27 @@ class WiringApp {
|
||||
api.wires.update(id, { waypoints: [] }).catch(console.error);
|
||||
this._flashSaved();
|
||||
}
|
||||
if (action === "copystyle" && wire) {
|
||||
this._copiedWireStyle = {
|
||||
color_primary: wire.color_primary,
|
||||
color_stripe: wire.color_stripe,
|
||||
gauge: wire.gauge,
|
||||
twisted_pair: wire.twisted_pair,
|
||||
twist_pitch: wire.twist_pitch,
|
||||
shielded: wire.shielded,
|
||||
show_size_label: wire.show_size_label,
|
||||
};
|
||||
}
|
||||
if (action === "pastestyle" && wire && this._copiedWireStyle) {
|
||||
Object.assign(wire, this._copiedWireStyle);
|
||||
this.canvas.updateWire(wire);
|
||||
await api.wires.update(id, this._copiedWireStyle).catch(console.error);
|
||||
if (this.canvas.selectedId === id) this._showWireProps(wire);
|
||||
this._flashSaved();
|
||||
}
|
||||
if (action === "selectnet" && wire?.label) {
|
||||
this.canvas.highlightNet(wire.label);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -701,9 +815,19 @@ class WiringApp {
|
||||
async _patchDevice(data, redraw = false) {
|
||||
if (this.canvas.selectedType !== "device") return;
|
||||
const id = this.canvas.selectedId;
|
||||
const d = this.canvas.deviceData.get(id);
|
||||
const oldData = d ? Object.fromEntries(Object.keys(data).map(k => [k, d[k]])) : {};
|
||||
if (d) Object.assign(d, data);
|
||||
await api.devices.update(id, data).catch(console.error);
|
||||
this._flashSaved();
|
||||
if (redraw) { const d = this.canvas.deviceData.get(id); if (d) { Object.assign(d, data); this.canvas.updateDevice(d); } }
|
||||
if (redraw && d) this.canvas.updateDevice(d);
|
||||
this._pushUndo(async () => {
|
||||
const cur = this.canvas.deviceData.get(id);
|
||||
if (cur) Object.assign(cur, oldData);
|
||||
await api.devices.update(id, oldData).catch(console.error);
|
||||
if (redraw && cur) this.canvas.updateDevice(cur);
|
||||
this._flashSaved();
|
||||
});
|
||||
}
|
||||
|
||||
async _patchDeviceProp(key, value, redraw = false) {
|
||||
@@ -711,16 +835,25 @@ class WiringApp {
|
||||
const id = this.canvas.selectedId;
|
||||
const d = this.canvas.deviceData.get(id);
|
||||
if (!d) return;
|
||||
d.properties = { ...(d.properties || {}), [key]: value };
|
||||
const oldProps = { ...(d.properties || {}) };
|
||||
d.properties = { ...oldProps, [key]: value };
|
||||
await api.devices.update(id, { properties: d.properties }).catch(console.error);
|
||||
this._flashSaved();
|
||||
if (redraw) { this.canvas.updateDevice(d); this.canvas.selectDevice(id); }
|
||||
this._pushUndo(async () => {
|
||||
const cur = this.canvas.deviceData.get(id);
|
||||
if (cur) { cur.properties = oldProps; }
|
||||
await api.devices.update(id, { properties: oldProps }).catch(console.error);
|
||||
if (redraw) { if (cur) this.canvas.updateDevice(cur); this.canvas.selectDevice(id); }
|
||||
this._flashSaved();
|
||||
});
|
||||
}
|
||||
|
||||
async _patchWire(data, redraw = false) {
|
||||
if (this.canvas.selectedType !== "wire") return;
|
||||
const id = this.canvas.selectedId;
|
||||
const w = this.canvas.wireData.get(id);
|
||||
const oldData = w ? Object.fromEntries(Object.keys(data).map(k => [k, w[k]])) : {};
|
||||
if (w) {
|
||||
Object.assign(w, data);
|
||||
if (redraw) this.canvas.updateWire(w);
|
||||
@@ -728,6 +861,12 @@ class WiringApp {
|
||||
try {
|
||||
await api.wires.update(id, data);
|
||||
this._flashSaved();
|
||||
this._pushUndo(async () => {
|
||||
const cur = this.canvas.wireData.get(id);
|
||||
if (cur) { Object.assign(cur, oldData); this.canvas.updateWire(cur); }
|
||||
await api.wires.update(id, oldData).catch(console.error);
|
||||
this._flashSaved();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Wire save failed:", e);
|
||||
this._flashError();
|
||||
@@ -996,6 +1135,7 @@ class WiringApp {
|
||||
|
||||
async _cloneDevices(sources, offset) {
|
||||
this.canvas.clearSelection();
|
||||
const clonedIds = [];
|
||||
for (const source of sources) {
|
||||
const def = {
|
||||
diagram_id: this.diagramId,
|
||||
@@ -1013,9 +1153,19 @@ class WiringApp {
|
||||
const device = await api.devices.create(def);
|
||||
this.canvas.addDevice(device);
|
||||
this.canvas.addToSelection(device.id);
|
||||
clonedIds.push(device.id);
|
||||
} catch (e) { console.error("Clone device failed:", e); }
|
||||
}
|
||||
this._flashSaved();
|
||||
this._pushUndo(async () => {
|
||||
this.canvas.clearSelection();
|
||||
this._clearProps();
|
||||
for (const cid of clonedIds) {
|
||||
this.canvas.removeDevice(cid);
|
||||
await api.devices.delete(cid).catch(console.error);
|
||||
}
|
||||
this._flashSaved();
|
||||
});
|
||||
if (this.canvas.selectedDeviceIds.size === 1) {
|
||||
const [id] = this.canvas.selectedDeviceIds;
|
||||
this.canvas.selectedId = id;
|
||||
|
||||
+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