Add views, import/duplicate, DRC, jumps toggle, multi-wire select, auto-route, delete view tabs
- Roadmap #22: File→Import JSON to restore a diagram (drag-drop or file picker) - Roadmap #23: Duplicate diagram from diagram list - Roadmap #24: DRC panel with collapsible categories (unconnected pins, dup refs, no length, no P/N) - Multi-view tabs: named views per diagram with independent device/wire layouts; snapshot-on-entry prevents bleed - Keyboard arrow nudge for selected device(s) - Undo for move, duplicate, label/property changes - Wire jump arcs toggle (⌒ Jumps button in toolbar) - Shift+click multi-wire selection; right-click bulk clear waypoints or auto-route - Auto-route: obstacle-avoiding ortho routing for single or bulk selected wires - Delete view tab with × button; right-click context menu for rename/delete Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+66
-6
@@ -33,6 +33,8 @@ class DiagramCanvas {
|
||||
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.selectedWireIds = new Set(); // wire IDs in multi-wire selection
|
||||
this.jumpsEnabled = true; // wire jump arcs at crossings
|
||||
|
||||
this._initStage(containerId);
|
||||
this._initZoomPan();
|
||||
@@ -639,6 +641,8 @@ class DiagramCanvas {
|
||||
clearSelection() {
|
||||
this._netHighlightIds.forEach(id => this._wireHighlight(id, false));
|
||||
this._netHighlightIds.clear();
|
||||
this.selectedWireIds.forEach(id => this._wireHighlight(id, false));
|
||||
this.selectedWireIds.clear();
|
||||
this.selectedDeviceIds.forEach(id => this._highlightDevice(id, false));
|
||||
this.selectedDeviceIds.clear();
|
||||
this._clearResizeHandles();
|
||||
@@ -966,7 +970,38 @@ class DiagramCanvas {
|
||||
|
||||
_makeSvgPath(pts, crossings) {
|
||||
if (this.routeMode === "curved") return this._ptsToSvgPathCurved(pts);
|
||||
return this._ptsToSvgPath(pts, crossings);
|
||||
return this._ptsToSvgPath(pts, this.jumpsEnabled ? crossings : null);
|
||||
}
|
||||
|
||||
setJumpsEnabled(enabled) {
|
||||
this.jumpsEnabled = enabled;
|
||||
this._recomputeJumps();
|
||||
}
|
||||
|
||||
autoRouteWire(wireId) {
|
||||
const wire = this.wireData.get(wireId);
|
||||
if (!wire) return null;
|
||||
const fd = this.deviceData.get(wire.from_device_id);
|
||||
const td = this.deviceData.get(wire.to_device_id);
|
||||
if (!fd || !td) return null;
|
||||
const fp = (fd.pins || []).find(p => p.id === wire.from_pin);
|
||||
const tp = (td.pins || []).find(p => p.id === wire.to_pin);
|
||||
if (!fp || !tp) return null;
|
||||
const S = 24;
|
||||
const ex1 = fd.x + fp.x_offset + (fp.side === "right" ? S : fp.side === "left" ? -S : 0);
|
||||
const ey1 = fd.y + fp.y_offset + (fp.side === "bottom" ? S : fp.side === "top" ? -S : 0);
|
||||
const ex2 = td.x + tp.x_offset + (tp.side === "right" ? S : tp.side === "left" ? -S : 0);
|
||||
const ey2 = td.y + tp.y_offset + (tp.side === "bottom" ? S : tp.side === "top" ? -S : 0);
|
||||
const excludeIds = new Set([wire.from_device_id, wire.to_device_id]);
|
||||
const inner = this._autoOrtho(ex1, ey1, ex2, ey2, excludeIds);
|
||||
// Strip the two exit points (_autoOrtho includes ex1,ey1 and ex2,ey2); store inner pairs as waypoints
|
||||
const mid = inner.slice(2, inner.length - 2);
|
||||
wire.waypoints = [];
|
||||
for (let i = 0; i < mid.length; i += 2) wire.waypoints.push({ x: mid[i], y: mid[i + 1] });
|
||||
this._destroyWireVisuals(wireId);
|
||||
this._renderWire(wire);
|
||||
this._recomputeJumps();
|
||||
return wire.waypoints;
|
||||
}
|
||||
|
||||
_recomputeJumps() {
|
||||
@@ -1630,24 +1665,49 @@ class DiagramCanvas {
|
||||
hit.on("click", (e) => {
|
||||
if (this.mode !== "select") return;
|
||||
e.cancelBubble = true;
|
||||
this.selectWire(wire.id);
|
||||
if (e.evt.shiftKey) {
|
||||
// Shift+click: toggle this wire in multi-wire selection
|
||||
if (this.selectedWireIds.has(wire.id)) {
|
||||
this.selectedWireIds.delete(wire.id);
|
||||
this._wireHighlight(wire.id, false);
|
||||
} else {
|
||||
// Add current single-selected wire to set first
|
||||
if (this.selectedId && this.selectedType === "wire" && !this.selectedWireIds.has(this.selectedId)) {
|
||||
this.selectedWireIds.add(this.selectedId);
|
||||
}
|
||||
this.selectedWireIds.add(wire.id);
|
||||
this._wireHighlight(wire.id, true);
|
||||
this.selectedId = wire.id;
|
||||
this.selectedType = "wire";
|
||||
}
|
||||
this.cb.onMultiWireSelected?.(this.selectedWireIds);
|
||||
} else {
|
||||
this.selectWire(wire.id);
|
||||
}
|
||||
});
|
||||
hit.on("mouseenter", () => {
|
||||
if (this.mode === "select" && this.selectedId !== wire.id) {
|
||||
if (this.mode === "select" && this.selectedId !== wire.id && !this.selectedWireIds.has(wire.id)) {
|
||||
this._wireHighlight(wire.id, true);
|
||||
this.stage.container().style.cursor = "pointer";
|
||||
}
|
||||
});
|
||||
hit.on("mouseleave", () => {
|
||||
if (this.selectedId !== wire.id) this._wireHighlight(wire.id, false);
|
||||
if (this.selectedId !== wire.id && !this.selectedWireIds.has(wire.id)) {
|
||||
this._wireHighlight(wire.id, false);
|
||||
}
|
||||
this.stage.container().style.cursor = "";
|
||||
});
|
||||
|
||||
hit.on("contextmenu", (e) => {
|
||||
e.evt.preventDefault();
|
||||
e.cancelBubble = true;
|
||||
if (this.selectedId !== wire.id) this.selectWire(wire.id);
|
||||
this.cb.onWireContextMenu?.(wire.id, e.evt.clientX, e.evt.clientY);
|
||||
if (this.selectedWireIds.size > 1 && this.selectedWireIds.has(wire.id)) {
|
||||
// Multi-wire context menu
|
||||
this.cb.onWireContextMenu?.(wire.id, e.evt.clientX, e.evt.clientY);
|
||||
} else {
|
||||
if (this.selectedId !== wire.id) this.selectWire(wire.id);
|
||||
this.cb.onWireContextMenu?.(wire.id, e.evt.clientX, e.evt.clientY);
|
||||
}
|
||||
});
|
||||
|
||||
// Wire body drag (selected wire only) → inserts a waypoint at drag start
|
||||
|
||||
Reference in New Issue
Block a user