Skip to content

Commit

Permalink
Render unconnected edges above nodes. (#9069)
Browse files Browse the repository at this point in the history
  • Loading branch information
kazcw committed Feb 19, 2024
1 parent f1d4e54 commit 760afbc
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 59 deletions.
4 changes: 2 additions & 2 deletions app/gui2/e2e/edgeInteractions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ test('Disconnect an edge from a port', async ({ page }) => {
await initGraph(page)
await expect(await edgesToNodeWithBinding(page, 'sum')).toHaveCount(2 * EDGE_PARTS)

const targetEdge = page.locator('path:nth-child(4)')
const targetEdge = page.locator('svg.behindNodes g:nth-child(2) path.visible')

// Hover over edge to the right of node with binding `ten`.
await targetEdge.click({
Expand All @@ -41,7 +41,7 @@ test('Connect an node to a port via dragging the edge', async ({ page }) => {
await initGraph(page)

await expect(await edgesToNodeWithBinding(page, 'sum')).toHaveCount(2 * EDGE_PARTS)
const targetEdge = page.locator('path:nth-child(4)')
const targetEdge = page.locator('svg.behindNodes g:nth-child(2) path.visible')
// Hover over edge to the left of node with binding `ten`.
await targetEdge.click({
position: { x: 450, y: 5.0 },
Expand Down
4 changes: 2 additions & 2 deletions app/gui2/e2e/edgeRendering.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ test('Hover behaviour of edges', async ({ page }) => {
await expect(hoveredEdgeElements).toHaveCount(SPLIT_EDGE_PARTS)

// Expect the top edge part to be dimmed
const topEdge = page.locator('path:nth-child(3)')
const topEdge = page.locator('svg.behindNodes g:nth-child(2) path:nth-child(1)')
await expect(topEdge).toHaveClass('edge visible dimmed')
// Expect the bottom edge part not to be dimmed
const bottomEdge = page.locator('path:nth-child(5)')
const bottomEdge = page.locator('svg.behindNodes g:nth-child(2) path:nth-child(3)')
await expect(bottomEdge).toHaveClass('edge visible')
})
11 changes: 1 addition & 10 deletions app/gui2/src/components/GraphEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -641,9 +641,7 @@ function handleEdgeDrop(source: AstId, position: Vec2) {
@nodeDoubleClick="(id) => stackNavigator.enterNode(id)"
/>
</div>
<svg :viewBox="graphNavigator.viewBox" class="svgBackdropLayer">
<GraphEdges @createNodeFromEdge="handleEdgeDrop" />
</svg>
<GraphEdges :navigator="graphNavigator" @createNodeFromEdge="handleEdgeDrop" />

<ComponentBrowser
v-if="componentBrowserVisible"
Expand Down Expand Up @@ -686,13 +684,6 @@ function handleEdgeDrop(source: AstId, position: Vec2) {
--node-color-no-type: #596b81;
}
.svgBackdropLayer {
position: absolute;
top: 0;
left: 0;
z-index: -1;
}
.htmlLayer {
position: absolute;
top: 0;
Expand Down
125 changes: 88 additions & 37 deletions app/gui2/src/components/GraphEditor/GraphEdge.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { injectGraphNavigator } from '@/providers/graphNavigator'
import { injectGraphSelection } from '@/providers/graphSelection'
import { useGraphStore, type Edge } from '@/stores/graph'
import { isConnected, useGraphStore, type Edge } from '@/stores/graph'
import { assert } from '@/util/assert'
import { Rect } from '@/util/data/rect'
import { Vec2 } from '@/util/data/vec2'
Expand All @@ -15,6 +15,7 @@ const graph = useGraphStore()
const props = defineProps<{
edge: Edge
maskSource?: boolean
}>()
const base = ref<SVGPathElement>()
Expand Down Expand Up @@ -61,16 +62,35 @@ const targetRect = computed<Rect | undefined>(() => {
return undefined
}
})
const sourceNodeRect = computed<Rect | undefined>(() => {
return sourceNode.value && graph.nodeRects.get(sourceNode.value)
})
const sourceRect = computed<Rect | undefined>(() => {
if (sourceNode.value != null) {
return graph.nodeRects.get(sourceNode.value)
if (sourceNodeRect.value) {
return sourceNodeRect.value
} else if (navigator?.sceneMousePos != null) {
return new Rect(navigator.sceneMousePos, Vec2.Zero)
} else {
return undefined
}
})
type NodeMask = {
id: string
rect: Rect
radius: number
}
const sourceMask = computed<NodeMask | undefined>(() => {
if (!props.maskSource) return
const rect = sourceNodeRect.value
if (!rect) return
const radius = 16
const id = `mask_for_edge_to-${props.edge.target ?? 'unconnected'}`
return { id, rect, radius }
})
const edgeColor = computed(
() =>
(targetNode.value && graph.db.getNodeColorStyle(targetNode.value)) ??
Expand Down Expand Up @@ -432,44 +452,75 @@ const arrowTransform = computed(() => {
if (pos != null) return `translate(${pos.x},${pos.y})`
else return undefined
})
const connected = computed(() => isConnected(props.edge))
</script>

<template>
<template v-if="basePath">
<path
v-if="activePath"
:d="basePath"
class="edge visible dimmed"
:style="baseStyle"
:data-source-node-id="sourceNode"
:data-target-node-id="targetNode"
/>
<path
:d="basePath"
class="edge io"
:data-source-node-id="sourceNode"
:data-target-node-id="targetNode"
@pointerdown="click"
@pointerenter="hovered = true"
@pointerleave="hovered = false"
/>
<path
ref="base"
:d="activePath ?? basePath"
class="edge visible"
:style="activePath ? activeStyle : baseStyle"
:data-source-node-id="sourceNode"
:data-target-node-id="targetNode"
/>
<polygon
v-if="arrowTransform"
:transform="arrowTransform"
points="0,-9.375 -9.375,9.375 9.375,9.375"
class="arrow visible"
:style="baseStyle"
:data-source-node-id="sourceNode"
:data-target-node-id="targetNode"
/>
<mask
v-if="sourceMask && navigator"
:id="sourceMask.id"
:x="navigator.viewport.left"
:y="navigator.viewport.top"
width="100%"
height="100%"
maskUnits="userSpaceOnUse"
>
<rect
:x="navigator.viewport.left"
:y="navigator.viewport.top"
width="100%"
height="100%"
fill="white"
/>
<rect
:x="sourceMask.rect.left"
:y="sourceMask.rect.top"
:width="sourceMask.rect.width"
:height="sourceMask.rect.height"
:rx="sourceMask.radius"
:ry="sourceMask.radius"
fill="black"
/>
</mask>
<g v-bind="sourceMask && { mask: `url('#${sourceMask.id}')` }">
<path
v-if="activePath"
:d="basePath"
class="edge visible dimmed"
:style="baseStyle"
:data-source-node-id="sourceNode"
:data-target-node-id="targetNode"
/>
<path
v-if="connected"
:d="basePath"
class="edge io"
:data-source-node-id="sourceNode"
:data-target-node-id="targetNode"
@pointerdown="click"
@pointerenter="hovered = true"
@pointerleave="hovered = false"
/>
<path
ref="base"
:d="activePath ?? basePath"
class="edge visible"
:style="activePath ? activeStyle : baseStyle"
:data-source-node-id="sourceNode"
:data-target-node-id="targetNode"
/>
<polygon
v-if="arrowTransform"
:transform="arrowTransform"
points="0,-9.375 -9.375,9.375 9.375,9.375"
class="arrow visible"
:style="baseStyle"
:data-source-node-id="sourceNode"
:data-target-node-id="targetNode"
/>
</g>
</template>
</template>
Expand Down
37 changes: 36 additions & 1 deletion app/gui2/src/components/GraphEditor/GraphEdges.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ const graph = useGraphStore()
const selection = injectGraphSelection(true)
const interaction = injectInteractionHandler()
const props = defineProps<{
navigator: GraphNavigator
}>()
const emits = defineEmits<{
createNodeFromEdge: [source: AstId, position: Vec2]
}>()
Expand Down Expand Up @@ -99,5 +103,36 @@ function createEdge(source: AstId, target: PortId) {
</script>

<template>
<GraphEdge v-for="(edge, index) in graph.edges" :key="index" :edge="edge" />
<div>
<svg :viewBox="props.navigator.viewBox" class="overlay behindNodes">
<GraphEdge v-for="edge in graph.connectedEdges" :key="edge.target" :edge="edge" />
</svg>
<svg
v-if="graph.unconnectedEdge"
:viewBox="props.navigator.viewBox"
class="overlay aboveNodes nonInteractive"
>
<GraphEdge :edge="graph.unconnectedEdge" maskSource />
</svg>
</div>
</template>

<style scoped>
.overlay {
position: absolute;
top: 0;
left: 0;
}
.overlay.behindNodes {
z-index: -1;
}
.overlay.aboveNodes {
z-index: 20;
}
.nonInteractive {
pointer-events: none;
}
</style>
34 changes: 27 additions & 7 deletions app/gui2/src/stores/graph/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,18 +186,30 @@ export const useGraphStore = defineStore('graph', () => {
return edges
})

const connectedEdges = computed(() => {
return edges.value.filter<ConnectedEdge>(isConnected)
})

function createEdgeFromOutput(source: Ast.AstId) {
unconnectedEdge.value = { source }
unconnectedEdge.value = { source, target: undefined }
}

function disconnectSource(edge: Edge) {
if (!edge.target) return
unconnectedEdge.value = { target: edge.target, disconnectedEdgeTarget: edge.target }
unconnectedEdge.value = {
source: undefined,
target: edge.target,
disconnectedEdgeTarget: edge.target,
}
}

function disconnectTarget(edge: Edge) {
if (!edge.source || !edge.target) return
unconnectedEdge.value = { source: edge.source, disconnectedEdgeTarget: edge.target }
unconnectedEdge.value = {
source: edge.source,
target: undefined,
disconnectedEdgeTarget: edge.target,
}
}

function clearUnconnected() {
Expand Down Expand Up @@ -542,6 +554,7 @@ export const useGraphStore = defineStore('graph', () => {
editedNodeInfo,
unconnectedEdge,
edges,
connectedEdges,
moduleSource,
nodeRects,
vizRects,
Expand Down Expand Up @@ -585,14 +598,21 @@ function randomIdent() {
}

/** An edge, which may be connected or unconnected. */
export type Edge = {
export interface Edge {
source: AstId | undefined
target: PortId | undefined
}

export type UnconnectedEdge = {
source?: AstId
target?: PortId
export interface ConnectedEdge extends Edge {
source: AstId
target: PortId
}

export function isConnected(edge: Edge): edge is ConnectedEdge {
return edge.source != null && edge.target != null
}

interface UnconnectedEdge extends Edge {
/** If this edge represents an in-progress edit of a connected edge, it is identified by its target expression. */
disconnectedEdgeTarget?: PortId
}
Expand Down

0 comments on commit 760afbc

Please sign in to comment.