Skip to content

Commit 6941b2c

Browse files
feat: render invisible devices as draggable circles (#245)
1. Non-visible devices cannot be selected Devices that are not visible (including drag circles) can no longer be selected, connected, or deleted. 2. Merged edges cannot be deleted Edges that are part of a merged group (i.e., connected to other edges) cannot be deleted. Only individual, unmerged edges can be removed. 3. Drag circles are for reorganization only The drag circle functionality is intended solely for moving and reorganizing devices. It is not possible to connect devices using drag circles. ![image](https://github.com/user-attachments/assets/8e4a7aec-81bf-44ae-aff6-8c72fd0fe671) ![image](https://github.com/user-attachments/assets/2f84e74d-74f2-4ed3-bd5c-d809f528b9a4) ![image](https://github.com/user-attachments/assets/5e1589f1-ed47-4292-bd5c-800deedd4348) --------- Co-authored-by: Tomás Grüner <[email protected]>
1 parent 70068d2 commit 6941b2c

File tree

6 files changed

+131
-22
lines changed

6 files changed

+131
-22
lines changed

src/graphics/renderables/multi_edge_info.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ export class MultiEdgeInfo extends BaseInfo {
2424
const fromDevice = edge.viewgraph.getDevice(edge.data.from.id);
2525
const toDevice = edge.viewgraph.getDevice(edge.data.to.id);
2626

27-
if (fromDevice && fromDevice.visible) {
27+
if (fromDevice && fromDevice.isVisible()) {
2828
visibleDevices.add(fromDevice.id);
2929
}
30-
if (toDevice && toDevice.visible) {
30+
if (toDevice && toDevice.isVisible()) {
3131
visibleDevices.add(toDevice.id);
3232
}
3333
});

src/graphics/renderables/program_info.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ function otherDevicesIp(viewgraph: ViewGraph, srcId: DeviceId) {
7676
.getDevices()
7777
.filter(
7878
(device) =>
79-
device.visible &&
79+
device.isVisible() &&
8080
device.getType() !== DeviceType.Switch &&
8181
device.id !== srcId,
8282
)

src/types/edge.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export class Edge extends Graphics {
6565
}
6666

6767
getDeviceIds(): DeviceId[] {
68+
if (!this.data) return [];
6869
return [this.data.from.id, this.data.to.id];
6970
}
7071

@@ -107,13 +108,26 @@ export class Edge extends Graphics {
107108

108109
select() {
109110
this.highlightedEdges = this.viewgraph.findConnectedEdges(this);
110-
this.highlightedEdges.forEach((edge) => edge.highlight());
111+
this.highlightedEdges.forEach((edge) => {
112+
edge.highlight();
113+
// Highlight the color of the circles connected to this edge
114+
edge.getDeviceIds().forEach((deviceId) => {
115+
const device = this.viewgraph.getDevice(deviceId);
116+
device.setCircleColor(Colors.Violet);
117+
});
118+
});
111119
this.showInfo();
112120
}
113121

114122
deselect() {
115-
// remove highlight from all edges
116-
this.highlightedEdges.forEach((edge) => edge.removeHighlight());
123+
this.highlightedEdges.forEach((edge) => {
124+
edge.removeHighlight();
125+
// Reset the color of the circles connected to this edge
126+
edge.getDeviceIds().forEach((deviceId) => {
127+
const device = this.viewgraph.getDevice(deviceId);
128+
device.setCircleColor(Colors.Lightblue);
129+
});
130+
});
117131
this.highlightedEdges = [];
118132
}
119133

@@ -126,7 +140,7 @@ export class Edge extends Graphics {
126140
}
127141

128142
showInfo() {
129-
if (this.highlightedEdges && this.highlightedEdges.length > 1) {
143+
if (this.isMerged()) {
130144
const multiEdgeInfo = new MultiEdgeInfo(this.highlightedEdges);
131145
RightBar.getInstance().renderInfo(multiEdgeInfo);
132146
} else {
@@ -135,6 +149,10 @@ export class Edge extends Graphics {
135149
}
136150
}
137151

152+
isMerged(): boolean {
153+
return this.highlightedEdges.length > 1;
154+
}
155+
138156
destroy(): void {
139157
deselectElement();
140158
this.removeTooltips();
@@ -188,8 +206,8 @@ export class Edge extends Graphics {
188206
const dy = device2.y - device1.y;
189207
const angle = Math.atan2(dy, dx);
190208

191-
const n1IsVisible = device1.visible;
192-
const n2IsVisible = device2.visible;
209+
const n1IsVisible = device1.isVisible();
210+
const n2IsVisible = device2.isVisible();
193211

194212
const offsetX1 = n1IsVisible
195213
? ((device1.width + 5) / 2) * Math.cos(angle)
@@ -299,7 +317,7 @@ export class Edge extends Graphics {
299317
// Check the visibility of the starting device
300318
const device1 = this.viewgraph.getDevice(this.data.from.id);
301319
if (this.startTooltip) {
302-
if (device1 && device1.visible) {
320+
if (device1 && device1.isVisible()) {
303321
// If the starting device is visible, update the tooltip position
304322
this.startTooltip.x =
305323
this.startPos.x + (isStartLeft ? offsetX : -offsetX);
@@ -314,7 +332,7 @@ export class Edge extends Graphics {
314332
// Check the visibility of the ending device
315333
const device2 = this.viewgraph.getDevice(this.data.to.id);
316334
if (this.endTooltip) {
317-
if (device2 && device2.visible) {
335+
if (device2 && device2.isVisible()) {
318336
// If the ending device is visible, update the tooltip position
319337
this.endTooltip.x = this.endPos.x + (isStartLeft ? -offsetX : offsetX);
320338
this.endTooltip.y = this.endPos.y + offsetY;

src/types/graphs/viewgraph.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -214,15 +214,20 @@ export class ViewGraph {
214214
// For each iteration, update visibility of all edges.
215215
// This takes into account currently visible edges and devices.
216216
for (const [, , edge] of this.graph.getAllEdges()) {
217-
const previousVisibility = edge.visible;
217+
const previousVisibility = edge.isVisible();
218218
edge.updateVisibility();
219-
hadChanges ||= previousVisibility !== edge.visible;
219+
hadChanges ||= previousVisibility !== edge.isVisible();
220220
}
221221
if (!hadChanges) {
222222
break;
223223
}
224224
}
225225

226+
// Update the devices aspect
227+
for (const [, device] of this.graph.getAllVertices()) {
228+
device.updateDevicesAspect();
229+
}
230+
226231
// warn Packet Manager that the layer has been changed
227232
this.packetManager.layerChanged(newLayer);
228233

@@ -250,7 +255,7 @@ export class ViewGraph {
250255

251256
const vertexFilter = (device: ViewDevice, id: DeviceId): boolean => {
252257
// If device is visible, add it to the set and stop traversal
253-
if (device.visible && id !== deviceId) {
258+
if (device.isVisible() && id !== deviceId) {
254259
visibleDevices.push(id);
255260
return false;
256261
}
@@ -277,15 +282,15 @@ export class ViewGraph {
277282
return false;
278283
}
279284
// If device is visible, add it to the set and stop traversal
280-
if (device.visible) {
285+
if (device.isVisible()) {
281286
visibleDevices.add(id);
282287
return false;
283288
}
284289
return true;
285290
};
286291

287292
// Avoid invisible edges
288-
const edgeFilter = (edge: Edge) => edge.visible;
293+
const edgeFilter = (edge: Edge) => edge.isVisible();
289294

290295
this.graph.dfs(startId, { vertexFilter, edgeFilter });
291296
return visibleDevices.size > 0;
@@ -501,7 +506,7 @@ export class ViewGraph {
501506

502507
for (const nodeId of currentEdge.getDeviceIds()) {
503508
const device = this.getDevice(nodeId);
504-
if (device && device.visible) {
509+
if (device && device.isVisible()) {
505510
continue;
506511
}
507512
const edges = this.getConnections(nodeId);

src/types/view-devices/vDevice.ts

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import {
3333
showTooltip,
3434
} from "../../graphics/renderables/canvas_tooltip_manager";
3535

36+
const CIRCLE_RADIUS = 6; // Radius of the circle for drag and drop
37+
3638
export enum DeviceType {
3739
Host = 0,
3840
Router = 1,
@@ -60,6 +62,10 @@ export function layerFromType(type: DeviceType) {
6062
export abstract class ViewDevice extends Container {
6163
private sprite: Sprite;
6264
private tooltip: Text | null = null; // Tooltip como un Text de PIXI.js
65+
private isDragCircle = false;
66+
private circleGraphic?: Graphics;
67+
private idLabel?: Text;
68+
private isVisibleFlag = true; // Flag to track visibility
6369

6470
readonly id: DeviceId;
6571
readonly viewgraph: ViewGraph;
@@ -124,10 +130,10 @@ export abstract class ViewDevice extends Container {
124130
this.interactive = true;
125131
this.cursor = "pointer";
126132
this.zIndex = ZIndexLevels.Device;
127-
this.updateVisibility();
128133

129134
// Add device ID label using the helper function
130135
this.addDeviceIdLabel();
136+
this.updateVisibility();
131137

132138
// Set up tooltip behavior
133139
this.setupHoverTooltip();
@@ -146,6 +152,7 @@ export abstract class ViewDevice extends Container {
146152

147153
private setupHoverTooltip() {
148154
this.on("mouseover", () => {
155+
if (this.isDragCircle) return;
149156
const currentLayer = this.ctx.getCurrentLayer();
150157
const tooltipMessage = this.getTooltipDetails(currentLayer);
151158
this.tooltip = showTooltip(
@@ -162,18 +169,81 @@ export abstract class ViewDevice extends Container {
162169
});
163170
}
164171

172+
setCircleColor(color: number) {
173+
if (!this.isDragCircle) return;
174+
if (this.circleGraphic) {
175+
this.circleGraphic.clear();
176+
this.circleGraphic.circle(0, 0, CIRCLE_RADIUS);
177+
this.circleGraphic.fill({ color });
178+
}
179+
}
180+
165181
/**
166182
* Abstract method to get tooltip details based on the layer.
167183
* Must be implemented by derived classes.
168184
*/
169185
abstract getTooltipDetails(layer: Layer): string;
170186

187+
updateDevicesAspect() {
188+
if (!this.isVisibleFlag) {
189+
const edges = this.viewgraph
190+
.getConnections(this.id)
191+
.filter((e) => e.isVisible());
192+
// if it doesn't have visible edges, hide it completely
193+
if (!edges || edges.length === 0) {
194+
this.visible = false;
195+
return;
196+
}
197+
// if it has visible edges, show it as a drag circle
198+
this.visible = true;
199+
this.setAsDragCircle();
200+
} else {
201+
// if it is in the current layer, show it as a normal device
202+
this.visible = true;
203+
this.setAsNormalDevice();
204+
}
205+
}
206+
171207
updateVisibility() {
172-
this.visible = layerIncluded(this.getLayer(), this.viewgraph.getLayer());
208+
this.isVisibleFlag = layerIncluded(
209+
this.getLayer(),
210+
this.viewgraph.getLayer(),
211+
);
212+
}
213+
214+
private setAsDragCircle() {
215+
if (this.isDragCircle) return;
216+
this.isDragCircle = true;
217+
218+
if (this.sprite) this.sprite.visible = false;
219+
if (this.idLabel) this.idLabel.visible = false;
220+
if (!this.circleGraphic) {
221+
this.circleGraphic = new Graphics();
222+
this.circleGraphic.circle(0, 0, CIRCLE_RADIUS);
223+
this.circleGraphic.fill({ color: Colors.Lightblue });
224+
this.addChild(this.circleGraphic);
225+
}
226+
this.eventMode = "static";
227+
this.interactive = true;
228+
this.cursor = "grab";
229+
}
230+
231+
private setAsNormalDevice() {
232+
if (!this.isDragCircle) return;
233+
this.isDragCircle = false;
234+
235+
if (this.sprite) this.sprite.visible = true;
236+
if (this.idLabel) this.idLabel.visible = true;
237+
if (this.circleGraphic) {
238+
this.removeChild(this.circleGraphic);
239+
this.circleGraphic.destroy();
240+
this.circleGraphic = undefined;
241+
}
242+
this.cursor = "pointer";
173243
}
174244

175245
isVisible(): boolean {
176-
return this.visible;
246+
return this.isVisibleFlag;
177247
}
178248

179249
// Function to add the ID label to the device
@@ -188,6 +258,7 @@ export abstract class ViewDevice extends Container {
188258
idText.anchor.set(0.5);
189259
idText.y = this.height * 0.8;
190260
idText.zIndex = ZIndexLevels.Label;
261+
this.idLabel = idText;
191262
this.addChild(idText); // Add the ID text as a child of the device
192263
}
193264

@@ -208,7 +279,6 @@ export abstract class ViewDevice extends Container {
208279
}
209280
ViewDevice.dragTarget = this;
210281

211-
// Guardar posición inicial
212282
ViewDevice.startPosition = { x: this.x, y: this.y };
213283
event.stopPropagation();
214284

@@ -229,6 +299,13 @@ export abstract class ViewDevice extends Container {
229299
ViewDevice.connectionTarget = null;
230300
return;
231301
}
302+
303+
// if the device is not visible, ignore
304+
if (!this.isVisibleFlag || !ViewDevice.connectionTarget.isVisibleFlag) {
305+
ViewDevice.connectionTarget = null;
306+
return;
307+
}
308+
232309
// Connect both devices
233310
const n1 = ViewDevice.connectionTarget.id;
234311
const n2 = this.id;
@@ -240,6 +317,10 @@ export abstract class ViewDevice extends Container {
240317
}
241318

242319
selectToConnect() {
320+
// if the device is not visible, do nothing
321+
if (!this.isVisibleFlag) {
322+
return;
323+
}
243324
if (ViewDevice.connectionTarget) {
244325
ViewDevice.connectionTarget = null;
245326
return;
@@ -287,6 +368,7 @@ export abstract class ViewDevice extends Container {
287368
}
288369

289370
select() {
371+
if (this.isDragCircle) return;
290372
this.highlight(); // Calls highlight on select
291373
this.showInfo();
292374
}

src/types/viewportManager.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const urManager = new UndoRedoManager();
2525
export function selectElement(element: Selectable) {
2626
deselectElement();
2727

28-
if (element) {
28+
if (element && element.isVisible()) {
2929
selectedElement = element;
3030
element.select();
3131
}
@@ -87,6 +87,10 @@ document.addEventListener("keydown", (event) => {
8787
const move = new RemoveDeviceMove(currLayer, selectedElement.id);
8888
urManager.push(viewgraph, move);
8989
} else if (isEdge(selectedElement)) {
90+
// if the edge is merged, do not delete it
91+
if (selectedElement.isMerged()) {
92+
return;
93+
}
9094
const ends = selectedElement.getDeviceIds();
9195
const move = new RemoveEdgeMove(currLayer, ends[0], ends[1]);
9296
urManager.push(viewgraph, move);

0 commit comments

Comments
 (0)