Skip to content

Commit e26399c

Browse files
Merge pull request #282 from wrayzheng/feat/edge-selection
More Efficient Edge Selection
2 parents 7fe1ba9 + 1ea3fbb commit e26399c

File tree

10 files changed

+213
-1
lines changed

10 files changed

+213
-1
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ This plugin enhances the Obsidian canvas with a wide array of features:
4545
* [Portals](#portals): Embed other canvases within your current canvas.
4646
* [Collapsible Groups](#collapsible-groups): Organize your canvas with expandable/collapsible groups.
4747
* [Edge Highlight](#edge-highlight): Highlight edges when a connected node is selected.
48+
* [Edge Selection](#edge-selection): Select edges connected to the selected node(s).
4849
* [Focus Mode](#focus-mode): Highlight a single node by blurring others.
4950
* [Encapsulate Selection](#encapsulate-selection): Move selected nodes to a new canvas, linking back to it.
5051
* Create groups independently of the nodes.
@@ -513,6 +514,28 @@ Flip the direction of an edge with one click.
513514
<img src="https://raw.githubusercontent.com/Developer-Mike/obsidian-advanced-canvas/main/assets/docs/flip-edge.gif" alt="Flip Edge Example"/>
514515
</details>
515516

517+
## Edge Selection
518+
Select edges connected to the selected node(s).
519+
520+
<details>
521+
<summary>Select Connected Edges Example</summary>
522+
<img src="https://raw.githubusercontent.com/Developer-Mike/obsidian-advanced-canvas/main/assets/docs/select-connected-edges.gif" alt="Select Connected Edges Example"/>
523+
</details>
524+
525+
### Select By Direction
526+
Select incoming or outgoing edges of the selected node(s).
527+
Note: this requires the setting `Edge Selection > Select Edge By Direction` to be enabled.
528+
529+
<details>
530+
<summary>Select Outgoing Edges Example</summary>
531+
<img src="https://raw.githubusercontent.com/Developer-Mike/obsidian-advanced-canvas/main/assets/docs/select-outgoing-edges.gif" alt="Select Outgoing Edges Example"/>
532+
</details>
533+
534+
<details>
535+
<summary>Select Incoming Edges Example</summary>
536+
<img src="https://raw.githubusercontent.com/Developer-Mike/obsidian-advanced-canvas/main/assets/docs/select-incoming-edges.gif" alt="Select Incoming Edges Example"/>
537+
</details>
538+
516539
## Canvas Events
517540
All custom events are prefixed with `advanced-canvas:` and can be listened to using `app.workspace.on` (just like default Obsidian events).
518541
Check out the list of events [here](https://github.com/Developer-Mike/obsidian-advanced-canvas/blob/main/src/%40types/CustomWorkspaceEvents.d.ts).
107 KB
Loading
133 KB
Loading
140 KB
Loading

src/canvas-extensions/commands-canvas-extension.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Canvas, CanvasNode } from "src/@types/Canvas"
1+
import { Canvas, CanvasEdge, CanvasNode } from "src/@types/Canvas"
22
import BBoxHelper from "src/utils/bbox-helper"
33
import CanvasHelper from "src/utils/canvas-helper"
44
import { FileSelectModal } from "src/utils/modal-helper"
@@ -129,6 +129,36 @@ export default class CommandsCanvasExtension extends CanvasExtension {
129129
)
130130
})
131131

132+
this.plugin.addCommand({
133+
id: 'select-connected-edges',
134+
name: 'Select connected edges',
135+
checkCallback: CanvasHelper.canvasCommand(
136+
this.plugin,
137+
(canvas: Canvas) => canvas.selection.size > 0,
138+
(canvas: Canvas) => CanvasHelper.selectEdgesForNodes(canvas, 'connected')
139+
)
140+
})
141+
142+
this.plugin.addCommand({
143+
id: 'select-incoming-edges',
144+
name: 'Select incoming edges',
145+
checkCallback: CanvasHelper.canvasCommand(
146+
this.plugin,
147+
(canvas: Canvas) => canvas.selection.size > 0,
148+
(canvas: Canvas) => CanvasHelper.selectEdgesForNodes(canvas, 'incoming')
149+
)
150+
})
151+
152+
this.plugin.addCommand({
153+
id: 'select-outgoing-edges',
154+
name: 'Select outgoing edges',
155+
checkCallback: CanvasHelper.canvasCommand(
156+
this.plugin,
157+
(canvas: Canvas) => canvas.selection.size > 0,
158+
(canvas: Canvas) => CanvasHelper.selectEdgesForNodes(canvas, 'outgoing')
159+
)
160+
})
161+
132162
this.plugin.addCommand({
133163
id: 'swap-nodes',
134164
name: 'Swap nodes',
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Canvas, CanvasEdge } from "src/@types/Canvas"
2+
import CanvasExtension from "./canvas-extension"
3+
import CanvasHelper, { ConnectionDirection, MenuOption } from "src/utils/canvas-helper"
4+
5+
const DIRECTION_MENU_MAP: Record<ConnectionDirection, MenuOption> = {
6+
connected: {
7+
id: 'select-connected-edges',
8+
icon: 'arrows-selected',
9+
label: 'Select Connected Edges',
10+
},
11+
outgoing: {
12+
id: 'select-outgoing-edges',
13+
icon: 'arrow-right-selected',
14+
label: 'Select Outgoing Edges',
15+
},
16+
incoming: {
17+
id: 'select-incoming-edges',
18+
icon: 'arrow-left-selected',
19+
label: 'Select Incoming Edges',
20+
},
21+
}
22+
23+
export default class EdgeSelectionCanvasExtension extends CanvasExtension {
24+
isEnabled() { return 'edgeSelectionEnabled' as const }
25+
26+
init() {
27+
this.plugin.registerEvent(this.plugin.app.workspace.on(
28+
'advanced-canvas:popup-menu-created',
29+
(canvas: Canvas) => this.onPopupMenuCreated(canvas)
30+
))
31+
}
32+
33+
private onPopupMenuCreated(canvas: Canvas) {
34+
const popupMenuEl = canvas?.menu?.menuEl
35+
if (!popupMenuEl) return
36+
37+
const selectionNodeData = canvas.getSelectionData().nodes
38+
if (canvas.readonly || selectionNodeData.length === 0) return
39+
40+
const selectEdgeByDirection = this.plugin.settings.getSetting("selectEdgeByDirection")
41+
const menuDirectionSet = new Set<ConnectionDirection>(['connected'])
42+
43+
if (selectionNodeData.length === 1) {
44+
// for better user experience and performance,
45+
// reduce unapplicable options for frequent use case
46+
const node = canvas.nodes.get(selectionNodeData[0].id)
47+
if (!node) return
48+
49+
const edges = canvas.getEdgesForNode(node)
50+
// hide all options if no edges
51+
if (edges.length === 0) return
52+
53+
// add options depending on edge types
54+
if (selectEdgeByDirection) {
55+
edges.forEach(edge => {
56+
if (edge.from.node === node) {
57+
menuDirectionSet.add('outgoing')
58+
} else if (edge.to.node === node) {
59+
menuDirectionSet.add('incoming')
60+
}
61+
})
62+
}
63+
} else if (selectEdgeByDirection) {
64+
// add all options if multiple nodes selected
65+
menuDirectionSet.add('outgoing')
66+
menuDirectionSet.add('incoming')
67+
}
68+
69+
menuDirectionSet.forEach(direction => {
70+
const config = DIRECTION_MENU_MAP[direction]
71+
CanvasHelper.addPopupMenuOption(canvas, CanvasHelper.createPopupMenuOption({
72+
...config,
73+
callback: () => CanvasHelper.selectEdgesForNodes(canvas, direction)
74+
}))
75+
})
76+
}
77+
}

src/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import CollapsibleGroupsCanvasExtension from './canvas-extensions/collapsible-gr
4141
import FocusModeCanvasExtension from './canvas-extensions/focus-mode-canvas-extension'
4242
import AutoFileNodeEdgesCanvasExtension from './canvas-extensions/auto-file-node-edges-canvas-extension'
4343
import FlipEdgeCanvasExtension from './canvas-extensions/flip-edge-canvas-extension'
44+
import EdgeSelectionCanvasExtension from './canvas-extensions/edge-selection-canvas-extension'
4445
import ExportCanvasExtension from './canvas-extensions/export-canvas-extension'
4546
import FloatingEdgeCanvasExtension from './canvas-extensions/floating-edge-canvas-extension'
4647
import EdgeHighlightCanvasExtension from './canvas-extensions/edge-highlight-canvas-extension'
@@ -101,6 +102,7 @@ const CANVAS_EXTENSIONS: typeof CanvasExtension[] = [
101102
ExportCanvasExtension,
102103
FocusModeCanvasExtension,
103104
EncapsulateCanvasExtension,
105+
EdgeSelectionCanvasExtension,
104106
]
105107

106108
export default class AdvancedCanvasPlugin extends Plugin {

src/settings.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ export interface AdvancedCanvasPluginSettingsValues {
102102

103103
edgeHighlightEnabled: boolean
104104
highlightIncomingEdges: boolean
105+
106+
edgeSelectionEnabled: boolean
107+
selectEdgeByDirection: boolean
105108
}
106109

107110
export const DEFAULT_SETTINGS_VALUES: AdvancedCanvasPluginSettingsValues = {
@@ -194,6 +197,9 @@ export const DEFAULT_SETTINGS_VALUES: AdvancedCanvasPluginSettingsValues = {
194197

195198
edgeHighlightEnabled: false,
196199
highlightIncomingEdges: false,
200+
201+
edgeSelectionEnabled: false,
202+
selectEdgeByDirection: false,
197203
}
198204

199205
export const SETTINGS = {
@@ -591,6 +597,18 @@ export const SETTINGS = {
591597
}
592598
}
593599
},
600+
edgeSelectionEnabled: {
601+
label: 'Edge selection',
602+
description: 'Select edges connected to the selected node(s) using the popup menu.',
603+
infoSection: 'edge-selection',
604+
children: {
605+
selectEdgeByDirection: {
606+
label: 'Select edge by direction',
607+
description: 'Select incoming or outgoing edges using separate popup menu items.',
608+
type: 'boolean'
609+
}
610+
}
611+
},
594612
focusModeFeatureEnabled: {
595613
label: 'Focus mode',
596614
description: 'Focus on a single node and blur all other nodes.',

src/utils/canvas-helper.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export interface MenuOption {
1212
callback?: () => void
1313
}
1414

15+
export type ConnectionDirection = 'connected' | 'outgoing' | 'incoming'
16+
1517
export default class CanvasHelper {
1618
static readonly GRID_SIZE = 20
1719

@@ -435,4 +437,34 @@ export default class CanvasHelper {
435437

436438
return bestSide!
437439
}
440+
441+
static selectEdgesForNodes(canvas: Canvas, direction: ConnectionDirection) {
442+
const selection = canvas.getSelectionData()
443+
if (selection.nodes.length === 0) return
444+
445+
const edges: Set<CanvasEdge> = new Set()
446+
447+
for (const nodeData of selection.nodes) {
448+
const node = canvas.nodes.get(nodeData.id)
449+
if (!node) continue
450+
451+
for (const edge of canvas.getEdgesForNode(node)) {
452+
switch (direction) {
453+
case 'connected':
454+
edges.add(edge)
455+
break
456+
case 'incoming':
457+
if (edge.to.node === node) edges.add(edge)
458+
break
459+
case 'outgoing':
460+
if (edge.from.node === node) edges.add(edge)
461+
break
462+
}
463+
}
464+
}
465+
466+
canvas.updateSelection(() => {
467+
canvas.selection = edges
468+
})
469+
}
438470
}

src/utils/icons-helper.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,36 @@ const CUSTOM_ICONS = {
3838

3939
'pathfinding-method-bezier': `<path stroke="currentColor" fill="none" stroke-width="8.5" d="M37.5 79.1667h35.4167a14.5833 14.5833 90 000-29.1667h-45.8333a14.5833 14.5833 90 010-29.1667H62.5"/>`,
4040
'pathfinding-method-square': `<path stroke="currentColor" fill="none" stroke-width="8.5" d="M72.9167 79.1667 72.9167 50 27.0833 50 27.0833 20.8333"/>`,
41+
42+
'arrows-selected': `
43+
<g stroke-width="2" stroke="currentColor" fill="none">
44+
<defs>
45+
<marker id="arrow-right" markerWidth="10" markerHeight="7" refX="2" refY="3.5" orient="auto"> <polygon points="2 2, 5 3.5, 2 5" /> </marker>
46+
<marker id="arrow-left" markerWidth="10" markerHeight="7" refX="4" refY="3.5" orient="auto"> <polygon points="1 3.5, 4 2, 4 5" /> </marker>
47+
</defs>
48+
<rect height="100" width="100" stroke-width="15" stroke="currentColor" stroke-dasharray="8,8" fill="transparent"/>
49+
<line x1="20" y1="30" x2="60" y2="30" stroke-width="5" marker-end="url(#arrow-right)"/>
50+
<line x1="40" y1="70" x2="80" y2="70" stroke-width="5" marker-start="url(#arrow-left)"/>
51+
</g>
52+
`,
53+
'arrow-right-selected': `
54+
<g stroke-width="2" stroke="currentColor" fill="none">
55+
<defs>
56+
<marker id="arrow-right" markerWidth="10" markerHeight="7" refX="2" refY="3.5" orient="auto"> <polygon points="2 2, 5 3.5, 2 5" /> </marker>
57+
</defs>
58+
<rect height="100" width="100" stroke-width="15" stroke="currentColor" stroke-dasharray="8,8" fill="transparent"/>
59+
<line x1="20" y1="50" x2="60" y2="50" stroke-width="5" marker-end="url(#arrow-right)"/>
60+
</g>
61+
`,
62+
'arrow-left-selected': `
63+
<g stroke-width="2" stroke="currentColor" fill="none">
64+
<defs>
65+
<marker id="arrow-left" markerWidth="10" markerHeight="7" refX="4" refY="3.5" orient="auto"> <polygon points="1 3.5, 4 2, 4 5" /> </marker>
66+
</defs>
67+
<rect height="100" width="100" stroke-width="15" stroke="currentColor" stroke-dasharray="8,8" fill="transparent"/>
68+
<line x1="40" y1="50" x2="80" y2="50" stroke-width="5" marker-start="url(#arrow-left)"/>
69+
</g>
70+
`,
4171
}
4272

4373
export default class IconsHelper {

0 commit comments

Comments
 (0)